UX-12# blueocean-rest implementation

- Embedded driver uses blueocean-rest module to expose service api implementation via REST
- Refactored core to move out embryo in to war module
- BlueOceanRootAction moved in to embedded driver
- README added to blueocean-rest and embedded-driver modules
This commit is contained in:
Vivek Pandey 2016-02-18 11:25:31 -08:00
parent 341bf52770
commit fbc607464a
54 changed files with 3074 additions and 145 deletions

View File

@ -1,33 +1,54 @@
package io.jenkins.blueocean.commons;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
/**
* @author Vivek Pandey
**/
public class JsonConverter{
private static final Logger LOGGER = LoggerFactory.getLogger(JsonConverter.class);
public static final String DATE_FORMAT_STRING = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
public static final ObjectMapper om = createObjectMapper();
public static <T> T toJava(String data, Class<T> type) {
try {
return om.readValue(data, type);
return toJava(new ByteArrayInputStream(data.getBytes("UTF-8")), type);
} catch (IOException e) {
throw new RuntimeException(String.format("Invalid json. Failed to convert json %s to type %s", data, type));
String msg = String.format("Failed to convert %s to type %s", data, type);
LOGGER.error(msg, e);
throw new ServiceException.UnexpectedErrorExpcetion(msg);
}
}
public static <T> T toJava(InputStream data, Class<T> type) {
try {
return om.readValue(data, type);
} catch (JsonParseException e){
String msg = "Json parsing failure: "+e.getMessage();
LOGGER.error(msg, e);
throw new ServiceException.BadRequestExpception("Json parsing failure: "+e.getMessage());
} catch (JsonMappingException e){
String msg = String.format("Failed to map Json to java type : %s. %s ", type, e.getMessage());
LOGGER.error(msg, e);
throw new ServiceException.UnexpectedErrorExpcetion(msg);
} catch (IOException e) {
throw new RuntimeException(String.format("Invalid json. Failed to convert %s to type %s", data, type));
String msg = String.format("Failed to convert %s to type %s", data, type);
LOGGER.error(msg, e);
throw new ServiceException.UnexpectedErrorExpcetion(msg);
}
}
@ -41,10 +62,11 @@ public class JsonConverter{
private static ObjectMapper createObjectMapper(){
ObjectMapper mapper = new ObjectMapper();
mapper.setDateFormat(new SimpleDateFormat(DATE_FORMAT_STRING));
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return mapper;
}
}

View File

@ -3,8 +3,8 @@ package io.jenkins.blueocean.commons;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.collect.ImmutableMap;
import edu.umd.cs.findbugs.annotations.NonNull;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@ -66,7 +66,7 @@ public class ServiceException extends RuntimeException{
public final int code;
public ErrorMessage(@NonNull Integer code, @NonNull String message) {
public ErrorMessage(@Nonnull Integer code, @Nonnull String message) {
this.code=code;
this.message = message;
}
@ -264,11 +264,48 @@ public class ServiceException extends RuntimeException{
}
}
public static final int BAD_REQUEST = 401;
public static class UnsupportedMediaTypeException extends ServiceException{
public UnsupportedMediaTypeException(String message) {
super(UNSUPPORTED_MEDIA_TYPE, message);
}
public UnsupportedMediaTypeException(String message, Throwable throwable ) {
super(UNSUPPORTED_MEDIA_TYPE, message, throwable);
}
public UnsupportedMediaTypeException(ErrorMessage errorMessage) {
super(UNSUPPORTED_MEDIA_TYPE, errorMessage.message);
}
public UnsupportedMediaTypeException(ErrorMessage errorMessage, Throwable throwable ) {
super(UNSUPPORTED_MEDIA_TYPE, errorMessage.message, throwable);
}
}
public static class MethodNotAllowedException extends ServiceException{
public MethodNotAllowedException(String message) {
super(METHOD_NOT_ALLOWED, message);
}
public MethodNotAllowedException(String message, Throwable throwable ) {
super(METHOD_NOT_ALLOWED, message, throwable);
}
public MethodNotAllowedException(ErrorMessage errorMessage) {
super(METHOD_NOT_ALLOWED, errorMessage.message);
}
public MethodNotAllowedException(ErrorMessage errorMessage, Throwable throwable ) {
super(METHOD_NOT_ALLOWED, errorMessage.message, throwable);
}
}
public static final int BAD_REQUEST = 400;
public static final int UNAUTHORIZED = 401;
public static final int FORBIDDEN = 403;
public static final int NOT_FOUND = 404;
public static final int METHOD_NOT_ALLOWED = 405;
public static final int UNSUPPORTED_MEDIA_TYPE = 415;
public static final int CONFLICT = 409;
public static final int UNPROCESSABLE_ENTITY = 422;
public static final int TOO_MANY_REQUESTS = 429;

View File

@ -0,0 +1,21 @@
package io.jenkins.blueocean.commons.guice;
import com.google.inject.AbstractModule;
import com.google.inject.matcher.Matchers;
import hudson.Extension;
import io.jenkins.blueocean.commons.JsonConverter;
/**
* Commons Guice module
*
* @author Vivek Pandey
*/
@Extension
public class CommonsModule extends AbstractModule {
@Override
protected void configure() {
bindListener(Matchers.any(), new Slf4jTypeListener());
bind(JsonConverter.class);
}
}

View File

@ -0,0 +1,14 @@
package io.jenkins.blueocean.commons.guice;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Vivek Pandey
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectLogger {
}

View File

@ -0,0 +1,30 @@
package io.jenkins.blueocean.commons.guice;
import com.google.inject.MembersInjector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
/**
* @author Vivek Pandey
*/
public class Slf4jMembersInjector<T> implements MembersInjector<T> {
private final Field field;
private final Logger logger;
Slf4jMembersInjector(Field aField) {
field = aField;
logger = LoggerFactory.getLogger(field.getDeclaringClass());
field.setAccessible(true);
}
@Override
public void injectMembers(T t) {
try {
field.set(t, logger);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,24 @@
package io.jenkins.blueocean.commons.guice;
import com.google.inject.TypeLiteral;
import com.google.inject.spi.TypeEncounter;
import com.google.inject.spi.TypeListener;
import org.slf4j.Logger;
import java.lang.reflect.Field;
/**
* @author Vivek Pandey
*/
public class Slf4jTypeListener implements TypeListener{
@Override
public <I> void hear(TypeLiteral<I> iTypeLiteral, TypeEncounter<I> iTypeEncounter) {
for (Field field : iTypeLiteral.getRawType().getDeclaredFields()) {
if (field.getType() == Logger.class
&& field.isAnnotationPresent(InjectLogger.class)) {
iTypeEncounter.register(new Slf4jMembersInjector<I>(field));
}
}
}
}

169
blueocean-rest/README.md Normal file
View File

@ -0,0 +1,169 @@
# Usage
## Run embedded-driver
cd embedded-driver
mvn hpi:run
This will launch jenkins with BO plugin.
BlueOcean UI is available at:
http://localhost:8080/jenkins/bo
BlueOcean rest API base URL is:
http://localhost:8080/jenkins/bo/rest
## Get a user
curl -v -X GET http://localhost:8080/jenkins/bo/rest/users/alice
{
"user" : {
"id" : "alice",
"name" : "Alice"
}
}
## Get user details
curl -v -X GET http://localhost:8080/jenkins/bo/rest/users/alice\?details\=true
{
"userDetails" : {
"id" : "alice",
"name" : "Alice",
"email" : "none"
}
}
> Backend needs to send email of user if present
## Find users in an organization
curl -v -X GET http://localhost:8080/jenkins/bo/rest/search\?q\=type:user\;organization:jenkins
{
"users" : [ {
"id" : "alice",
"name" : "Alice"
} ]
}
## Get organization details
curl -v -X GET http://localhost:8080/jenkins/bo/rest/organizations/jenkins
{
"organization" : {
"name" : "jenkins"
}
}
## Get a Pipeline
curl -v -X GET "http://localhost:8080/jenkins/bo/rest/organizations/jenkins/pipelines/test1"
{
"pipeline" : {
"organization" : "jenkins",
"name" : "test1",
"branches" : [ ]
}
}
## Get Pipelines
curl -v -X GET http://localhost:8080/jenkins/bo/rest/organizations/jenkins/pipelines
{
"pipelines" : [ {
"organization" : "jenkins",
"name" : "test1",
"branches" : [ ]
} ]
}
## Get all runs in a pipeline
curl -v -X GET http://localhost:8080/jenkins/bo/rest/organizations/jenkins/pipelines/test1/runs
{
"runs" : [ {
"id" : "2",
"pipeline" : "test1",
"organization" : "jenkins",
"status" : "SUCCESSFUL",
"startTime" : "2016-02-19T11:14:53.074Z",
"enQueueTime" : "2016-02-19T11:14:53.072Z",
"endTime" : "2016-02-19T11:14:53.109Z",
"durationInMillis" : 35,
"runSummary" : "stable",
"result" : {
"type" : "FreeStyleBuild",
"data" : { }
}
}, {
"id" : "1",
"pipeline" : "test1",
"organization" : "jenkins",
"status" : "SUCCESSFUL",
"startTime" : "2016-02-18T19:39:36.679Z",
"enQueueTime" : "2016-02-18T19:39:36.674Z",
"endTime" : "2016-02-18T19:39:36.747Z",
"durationInMillis" : 68,
"runSummary" : "stable",
"result" : {
"type" : "FreeStyleBuild",
"data" : { }
}
} ]
}
## Get latest run of a pipeline
curl -v -X GET http://localhost:8080/jenkins/bo/rest/organizations/jenkins/pipelines/test1/runs\?latestOnly\=true
{
"runs" : [ {
"id" : "2",
"pipeline" : "test1",
"organization" : "jenkins",
"status" : "SUCCESSFUL",
"startTime" : "2016-02-19T11:14:53.074Z",
"enQueueTime" : "2016-02-19T11:14:53.072Z",
"endTime" : "2016-02-19T11:14:53.109Z",
"durationInMillis" : 35,
"runSummary" : "stable",
"result" : {
"type" : "FreeStyleBuild",
"data" : { }
}
} ]
}
## Get a run details
curl -v -X GET http://localhost:8080/jenkins/bo/rest/organizations/jenkins/pipelines/test2/runs/1
{
"run" : {
"id" : "1",
"pipeline" : "test2",
"organization" : "jenkins",
"status" : "SUCCESSFUL",
"startTime" : "2016-02-19T11:14:41.414Z",
"enQueueTime" : "2016-02-19T11:14:41.411Z",
"endTime" : "2016-02-19T11:14:41.482Z",
"durationInMillis" : 68,
"runSummary" : "stable",
"result" : {
"type" : "FreeStyleBuild",
"data" : { }
}
}
}

50
blueocean-rest/pom.xml Normal file
View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.jenkins.blueocean</groupId>
<artifactId>blueocean-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>blueocean-rest</artifactId>
<packaging>hpi</packaging>
<name>BlueOcean :: Alpha Plugin</name>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>blueocean-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>blueocean-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>blueocean-security-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>profile-service-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>pipeline-service-api</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,222 @@
package io.jenkins.blueocean.rest;
import hudson.Extension;
import hudson.ExtensionList;
import io.jenkins.blueocean.api.pipeline.FindPipelineRunsRequest;
import io.jenkins.blueocean.api.pipeline.FindPipelinesRequest;
import io.jenkins.blueocean.api.pipeline.GetPipelineRequest;
import io.jenkins.blueocean.api.pipeline.GetPipelineRunRequest;
import io.jenkins.blueocean.api.pipeline.PipelineService;
import io.jenkins.blueocean.api.profile.CreateOrganizationRequest;
import io.jenkins.blueocean.api.profile.CreateOrganizationResponse;
import io.jenkins.blueocean.api.profile.FindUsersRequest;
import io.jenkins.blueocean.api.profile.GetOrganizationRequest;
import io.jenkins.blueocean.api.profile.GetUserDetailsRequest;
import io.jenkins.blueocean.api.profile.GetUserRequest;
import io.jenkins.blueocean.api.profile.ProfileService;
import io.jenkins.blueocean.commons.ServiceException;
import io.jenkins.blueocean.commons.guice.InjectLogger;
import io.jenkins.blueocean.rest.router.Route;
import io.jenkins.blueocean.rest.router.RouteContext;
import io.jenkins.blueocean.rest.router.Router;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.slf4j.Logger;
import java.util.List;
/**
* Entrypoint for blueocean REST apis. $CONTEXT_PATH/rest being root. e.g. /jenkins/rest
*
*
* @author Vivek Pandey
*/
@Extension
public final class ApiHead{
@InjectLogger
private Logger logger;
private final ProfileService profileService;
private final PipelineService pipelineService;
private static final String USER_ID_PARAM=":user-id";
private static final String ORGANIZATION_ID_PARAM=":organization-id";
private static final String PIPELINE_ID_PARAM=":pipeline-id";
private static final String RUN_ID_PARAM=":run-id";
private static final String ACCEPT_TYPE_REQUEST_MIME_HEADER = "Accept";
public ApiHead() {
this.profileService = getService(ProfileService.class);
this.pipelineService = getService(PipelineService.class);
Router.get(new Route.RouteImpl(String.format("users/%s",USER_ID_PARAM)) {
@Override
public Object handle(Request request, Response response) {
response.status(200);
Boolean details = request.queryParam("details", Boolean.class);
if(details != null && details) {
return profileService.getUserDetails(request.principal(),
new GetUserDetailsRequest(request.pathParam(USER_ID_PARAM)));
}else {
return profileService.getUser(request.principal(),
new GetUserRequest(request.pathParam(USER_ID_PARAM)));
}
}
});
Router.get(new Route.RouteImpl(String.format("users/%s",ORGANIZATION_ID_PARAM)) {
@Override
public Object handle(Request request, Response response) {
response.status(200);
return profileService.getOrganization(request.principal(),
new GetOrganizationRequest(request.pathParam(ORGANIZATION_ID_PARAM)));
}
});
Router.get(new Route.RouteImpl(String.format("organizations/%s",ORGANIZATION_ID_PARAM)) {
@Override
public Object handle(Request request, Response response) {
response.status(200);
return profileService.getOrganization(request.principal(),
new GetOrganizationRequest(request.pathParam(ORGANIZATION_ID_PARAM)));
}
});
Router.post(new Route.RouteImpl("organizations") {
@Override
public Object handle(Request request, Response response) {
CreateOrganizationResponse createOrganizationResponse = profileService.createOrganization(request.principal(),
new CreateOrganizationRequest(request.pathParam(ORGANIZATION_ID_PARAM)));
response.status(201);
response.header("Location", appendSlashToPath(request.uri())+createOrganizationResponse.organization);
return createOrganizationResponse;
}
});
Router.get(new Route.RouteImpl(String.format("organizations/%s/pipelines", ORGANIZATION_ID_PARAM)){
@Override
public Object handle(Request request, Response response) {
response.status(200);
return pipelineService.findPipelines(request.principal(),
new FindPipelinesRequest(request.pathParam(ORGANIZATION_ID_PARAM), null));
}
});
Router.get(new Route.RouteImpl(String.format("organizations/%s/pipelines/%s",
ORGANIZATION_ID_PARAM, PIPELINE_ID_PARAM)){
@Override
public Object handle(Request request, Response response) {
response.status(200);
return pipelineService.getPipeline(request.principal(),
new GetPipelineRequest(request.pathParam(ORGANIZATION_ID_PARAM),
request.pathParam(PIPELINE_ID_PARAM)));
}
});
Router.get(new Route.RouteImpl(String.format("organizations/%s/pipelines/%s/runs",
ORGANIZATION_ID_PARAM, PIPELINE_ID_PARAM)){
@Override
public Object handle(Request request, Response response) {
response.status(200);
return pipelineService.findPipelineRuns(request.principal(),
new FindPipelineRunsRequest(request.pathParam(ORGANIZATION_ID_PARAM),
request.pathParam(PIPELINE_ID_PARAM),
request.queryParam("latestOnly", Boolean.class),null,null,null));
}
});
Router.get(new Route.RouteImpl(String.format("organizations/%s/pipelines/%s/runs/%s",
ORGANIZATION_ID_PARAM, PIPELINE_ID_PARAM, RUN_ID_PARAM)){
@Override
public Object handle(Request request, Response response) {
response.status(200);
return pipelineService.getPipelineRun(request.principal(),
new GetPipelineRunRequest(request.pathParam(ORGANIZATION_ID_PARAM),
request.pathParam(PIPELINE_ID_PARAM), request.pathParam(RUN_ID_PARAM)));
}
});
Router.get(new Route.RouteImpl("search"){
@Override
@SuppressWarnings({"unchecked"})
public Object handle(Request request, Response response) {
response.status(200);
Query query = Query.parse(request.queryParam("q", true));
assert query != null; //will never be the case since its taken care in request.queryParam()
switch (query.type){
case "user":
return profileService.findUsers(request.principal(),
new FindUsersRequest(query.param("organization", true),
query.param("start", Long.class),
query.param("limit", Long.class)));
case "pipeline":
return pipelineService.findPipelines(request.principal(),
new FindPipelinesRequest(query.param("organization", true), query.param("pipeline")));
case "run":
return pipelineService.findPipelineRuns(request.principal(),
new FindPipelineRunsRequest(query.param("organization", true),
query.param("pipeline"),
query.param("latestOnly", Boolean.class),
query.param("branches", List.class),
query.param("start", Long.class),
query.param("limit", Long.class)));
default:
throw new ServiceException.BadRequestExpception("Unknown query type: "+query.type);
}
}
});
}
public HttpResponse doDynamic(StaplerRequest request, StaplerResponse response){
String method = request.getMethod().toLowerCase();
String url = request.getOriginalRestOfPath();
String acceptType = request.getHeader(ACCEPT_TYPE_REQUEST_MIME_HEADER);
Body body = new Body();
HttpMethod httpMethod = HttpMethod.get(method);
RouteContext context = RouteContext.create().withAcceptType(acceptType).withBody(body)
.withHttpMethod(httpMethod).withMatcher(Router.INSTANCE.routeMatcher())
.withResponse(new Response(response)).withUri(url).withHttpRequest(request);
try{
Router.execute(context);
return JsonHttpResponse.json(context.body().get());
}catch (ServiceException e){
if(e.status > 499) {
logger.error(e.getMessage(), e);
}
return JsonHttpResponse.json(e.errorMessage, e.status);
}catch (Exception e){
logger.error(e.getMessage(), e);
return JsonHttpResponse.json(new ServiceException.ErrorMessage(500, "Unexpected error"), 500);
}
}
private <T> T getService(Class<T> type){
ExtensionList<T> services = Jenkins.getActiveInstance().getExtensionList(type);
if(services.isEmpty()){
throw new ServiceException.UnexpectedErrorExpcetion("No implementation found for service API: " + type);
}
return services.get(0);
}
private String appendSlashToPath(String path){
return path.endsWith("/") ? path : path + "/";
}
}

View File

@ -0,0 +1,30 @@
package io.jenkins.blueocean.rest;
/**
* Wrapper over http body content
*
* @author Vivek Pandey
*/
public class Body {
private Object content;
public Body() {
}
public boolean isSet(){
return content != null;
}
public Object get(){
return content;
}
public void set(Object content){
this.content = content;
}
public boolean notSet() {
return content == null;
}
}

View File

@ -0,0 +1,39 @@
package io.jenkins.blueocean.rest;
import io.jenkins.blueocean.rest.router.Route;
/**
* Filter is just another Route
*
* @author Per Wendel
* @author Vivek Pandey
*/
public interface Filter extends Route{
abstract class FilterImpl implements Filter{
private final String path;
private final String acceptType;
private final String contentType;
public FilterImpl(String path) {
this.path = path;
this.acceptType = Route.DEFAULT_ACCEPT_TYPE;
this.contentType = Route.DEFAULT_CONTENT_TYPE;
}
@Override
public String path() {
return path;
}
@Override
public String accessType() {
return acceptType;
}
@Override
public String contentType() {
return contentType;
}
}
}

View File

@ -0,0 +1,27 @@
package io.jenkins.blueocean.rest;
import java.util.HashMap;
/**
* @author Per Wendel
*/
public enum HttpMethod {
get, post, put, patch, delete, head, trace, connect, options, before, after, unsupported;
private static HashMap<String, HttpMethod> methods = new HashMap();
static {
for (HttpMethod method : values()) {
methods.put(method.toString(), method);
}
}
/**
* Gets the HttpMethod corresponding to the provided string. If no corresponding method can be found
* {@link HttpMethod#unsupported} will be returned.
*/
public static HttpMethod get(String methodStr) {
HttpMethod method = methods.get(methodStr);
return method != null ? method : unsupported;
}
}

View File

@ -0,0 +1,38 @@
package io.jenkins.blueocean.rest;
import io.jenkins.blueocean.commons.JsonConverter;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import javax.servlet.ServletException;
import java.io.IOException;
/**
* @author Vivek Pandey
**/
public class JsonHttpResponse{
public static final String MEDIA_TYPE_APPLICATION_JSON = "application/json;charset=UTF-8";
public static HttpResponse json(final Object value, final int status) {
return new HttpResponse() {
public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws
IOException, ServletException {
rsp.setContentType(MEDIA_TYPE_APPLICATION_JSON);
rsp.getWriter().print(JsonConverter.toJson(value));
rsp.setStatus(status);
}
};
}
public static HttpResponse json(final Object value) {
return new HttpResponse() {
public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws
IOException, ServletException {
rsp.setContentType(MEDIA_TYPE_APPLICATION_JSON);
rsp.getWriter().print(JsonConverter.toJson(value));
rsp.setStatus(200);
}
};
}
}

View File

@ -0,0 +1,81 @@
package io.jenkins.blueocean.rest;
import com.google.common.collect.ImmutableMap;
import io.jenkins.blueocean.commons.ServiceException;
import java.util.HashMap;
import java.util.Map;
/**
* Query syntax
*
* Give all pipelines for acme organization
*
* <pre>/search?q=type:pipeline organization:acme</pre>
*<p>
* Give all Runs for pipelineA in organization acme
*
* <pre>/search?q=type:run pipeline:pipelineA organization:acme</pre>
*
* @author Vivek Pandey
*/
public class Query {
public final String type;
private final Map<String, String> params;
private Query(String type, Map<String, String> values) {
this.type = type;
this.params = (values == null) ? ImmutableMap.<String, String>of() : ImmutableMap.copyOf(values);
}
public String param(String key){
return params.get(key);
}
public String param(String key, boolean required){
return param(key, String.class, required);
}
public <T> T param(String key, Class<T> type){
return param(key, type, false);
}
public <T> T param(String key, Class<T> type, boolean required){
String value = params.get(key);
if(value == null && required){
throw new ServiceException.BadRequestExpception(
String.format("%s is required parameter to execute query for type %s", key, type));
}else if (value == null){
return null;
}
return Utils.cast(value, type);
}
public static Query parse(String query) {
String[] values = query.split(";");
String type = null;
Map<String, String> params = new HashMap<>();
for (String v : values) {
String[] vv = v.split(":");
if (vv.length != 2) {
continue;
}
String key = vv[0].trim().toLowerCase();
String value = vv[1].trim();
if (key.equals("type")) {
type = value.toLowerCase();
} else {
params.put(key, value);
}
}
//If there is no type, we don't know what resource to look for
//TODO: what would be default type?
if (type == null) {
throw new ServiceException.BadRequestExpception("type is required parameter for query param q");
}
return new Query(type, params);
}
}

View File

@ -0,0 +1,448 @@
package io.jenkins.blueocean.rest;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import hudson.util.QueryParameterMap;
import io.jenkins.blueocean.commons.JsonConverter;
import io.jenkins.blueocean.commons.ServiceException;
import io.jenkins.blueocean.rest.router.RouteMatch;
import io.jenkins.blueocean.security.Identity;
import javax.annotation.Nonnull;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.InputStream;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author Vivek Pandey
*/
public class Request {
private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(Request.class);
private static final String USER_AGENT = "user-agent";
private final Map<String, String> params;
private final List<String> splat;
private final QueryParameterMap queryMap;
private final HttpServletRequest servletRequest;
private Session session = null;
private final Set<String> headers = new HashSet<>();
private InputStream body;
/**
* Constructor
*
* @param match the route match
* @param request the servlet request
*/
@SuppressWarnings("unchecked")
public Request(RouteMatch match, HttpServletRequest request) {
this.servletRequest = request;
Enumeration<String> enumeration = servletRequest.getHeaderNames();
while (enumeration.hasMoreElements()) {
headers.add(enumeration.nextElement());
}
this.queryMap = new QueryParameterMap(request);
List<String> requestList = Utils.convertRouteToList(match.getRequestURI());
List<String> matchedList = Utils.convertRouteToList(match.getMatchUri());
this.params = getParams(requestList, matchedList);
this.splat = getSplat(requestList, matchedList);
try {
this.body = request.getInputStream();
} catch (IOException e) {
throw new ServiceException.UnexpectedErrorExpcetion("Failed to read request input stream: "+e.getMessage(), e);
}
this.session = new Session(servletRequest.getSession());
}
/**
* Returns the map containing all route pathParam
*
* @return a map containing all route pathParam
*/
public Map<String, String> pathParam() {
return params;
}
/**
* Returns the value of the provided route pattern parameter.
* Example: parameter 'name' from the following pattern: (get '/hello/:name')
*
* @param param the param
* @return null if the given param is null or not found
*/
public String pathParam(String param) {
if (param == null) {
return null;
}
if (param.startsWith(":")) {
return params.get(param.toLowerCase()); // NOSONAR
} else {
return params.get(":" + param.toLowerCase()); // NOSONAR
}
}
@SuppressWarnings("unchecked")
public <T> T pathParam(String param, Class<T> type) {
if (param == null) {
return null;
}
String value;
if (param.startsWith(":")) {
value = params.get(param.toLowerCase()); // NOSONAR
} else {
value = params.get(":" + param.toLowerCase()); // NOSONAR
}
return Utils.cast(value, type);
}
public String queryParam(@Nonnull String key) {
return queryParam(key, String.class, false);
}
public String queryParam(@Nonnull String key, boolean required) {
return queryParam(key, String.class, required);
}
@SuppressWarnings("unchecked")
public <T> T queryParam(@Nonnull String key, @Nonnull Class<T> type) {
return queryParam(key, type, false);
}
@SuppressWarnings("unchecked")
public <T> T queryParam(@Nonnull String key, @Nonnull Class<T> type, boolean required) {
String value = queryMap.get(key);
if (value == null && required) {
throw new ServiceException.BadRequestExpception(String.format("Query param %s is required", key));
}
if (value == null) {
if (Boolean.class.isAssignableFrom(type)) {
return (T) Boolean.FALSE;
} else {
return null;
}
}
return Utils.cast(value, type);
}
/**
* @return an array containing the splat (wildcard) parameters
*/
public String[] splat() {
return splat.toArray(new String[splat.size()]);
}
/**
* @return request method e.g. GET, POST, PUT, ...
*/
public String requestMethod() {
return servletRequest.getMethod();
}
/**
* @return the scheme
*/
public String scheme() {
return servletRequest.getScheme();
}
/**
* @return the host
*/
public String host() {
return servletRequest.getHeader("host");
}
/**
* @return the user-agent
*/
public String userAgent() {
return servletRequest.getHeader(USER_AGENT);
}
/**
* @return the server port
*/
public int port() {
return servletRequest.getServerPort();
}
/**
* @return the path info
* Example return: "/example/foo"
*/
public String pathInfo() {
return servletRequest.getPathInfo();
}
/**
* @return the servlet path
*/
public String servletPath() {
return servletRequest.getServletPath();
}
/**
* @return the context path
*/
public String contextPath() {
return servletRequest.getContextPath();
}
/**
* @return the URL string
*/
public String url() {
return servletRequest.getRequestURL().toString();
}
/**
* @return the content type of the body
*/
public String contentType() {
return servletRequest.getContentType();
}
/**
* @return the client's IP address
*/
public String ip() {
return servletRequest.getRemoteAddr();
}
public Identity principal(){
Principal principal = servletRequest.getUserPrincipal();
if(principal != null && principal instanceof Identity){
return (Identity) principal;
}else{
return Identity.ANONYMOUS;
}
}
public <T> T body(@Nonnull String contentType, @Nonnull Class<T> type){
if(contentType.toLowerCase().startsWith("application/json")){
return JsonConverter.toJava(body, type);
}else{
throw new ServiceException.UnsupportedMediaTypeException("Only application/json supported at the moment");
}
}
/**
* @return the length of request.body
*/
public int contentLength() {
return servletRequest.getContentLength();
}
/**
* Gets the value for the provided header
*
* @param header the header
* @return the value of the provided header
*/
public String headers(String header) {
return servletRequest.getHeader(header);
}
/**
* @return all headers
*/
public Set<String> headers() {
return ImmutableSet.copyOf(headers);
}
/**
* @return the query string
*/
public String queryString() {
return servletRequest.getQueryString();
}
/**
* Sets an attribute on the request (can be fetched in filters/routes later in the chain)
*
* @param attribute The attribute
* @param value The attribute value
*/
public void attribute(String attribute, Object value) {
servletRequest.setAttribute(attribute, value);
}
/**
* @return all attributes
*/
@SuppressWarnings("unchecked")
public Set<String> attributes() {
Set<String> attrList = new HashSet<String>();
Enumeration<String> attributes = (Enumeration<String>) servletRequest.getAttributeNames();
while (attributes.hasMoreElements()) {
attrList.add(attributes.nextElement());
}
return attrList;
}
/**
* @return the raw HttpServletRequest object handed in by servlet container
*/
public HttpServletRequest raw() {
return servletRequest;
}
/**
* @return the query map
*/
public QueryParameterMap queryMap() {
return queryMap;
}
public boolean hasQueryParameter(){
return servletRequest.getParameterNames().hasMoreElements();
}
/**
* Returns the current session associated with this request,
* or if the request does not have a session, creates one.
*
* @return the session associated with this request
*/
public Session session() {
return session;
}
/**
* Returns the current session associated with this request, or if there is
* no current session and <code>create</code> is true, returns a new session.
*
* @param create <code>true</code> to create a new session for this request if necessary;
* <code>false</code> to return null if there's no current session
* @return the session associated with this request or <code>null</code> if
* <code>create</code> is <code>false</code> and the request has no valid session
*/
public Session session(boolean create) {
if (session == null) {
HttpSession httpSession = servletRequest.getSession(create);
if (httpSession != null) {
session = new Session(httpSession);
}
}
return session;
}
/**
* @return request cookies (or empty Map if cookies aren't present)
*/
public Map<String, String> cookies() {
Map<String, String> result = new HashMap<String, String>();
Cookie[] cookies = servletRequest.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
result.put(cookie.getName(), cookie.getValue());
}
}
return result;
}
/**
* Gets cookie by name.
*
* @param name name of the cookie
* @return cookie value or null if the cookie was not found
*/
public String cookie(String name) {
Cookie[] cookies = servletRequest.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(name)) {
return cookie.getValue();
}
}
}
return null;
}
/**
* @return the part of this request's URL from the protocol name up to the query string in the first line of the HTTP request.
*/
public String uri() {
return servletRequest.getRequestURI();
}
/**
* @return Returns the name and version of the protocol the request uses
*/
public String protocol() {
return servletRequest.getProtocol();
}
/** Returns unmodifiable param map */
private static Map<String, String> getParams(List<String> request, List<String> matched) {
LOG.debug("get pathParam");
Map<String, String> params = new HashMap<String, String>();
for (int i = 0; (i < request.size()) && (i < matched.size()); i++) {
String matchedPart = matched.get(i);
if (Utils.isParam(matchedPart)) {
LOG.debug("matchedPart: "
+ matchedPart
+ " = "
+ request.get(i));
params.put(matchedPart.toLowerCase(), request.get(i));
}
}
return ImmutableMap.copyOf(params);
}
private static List<String> getSplat(List<String> request, List<String> matched) {
LOG.debug("get splat");
int nbrOfRequestParts = request.size();
int nbrOfMatchedParts = matched.size();
boolean sameLength = (nbrOfRequestParts == nbrOfMatchedParts);
List<String> splat = new ArrayList<String>();
for (int i = 0; (i < nbrOfRequestParts) && (i < nbrOfMatchedParts); i++) {
String matchedPart = matched.get(i);
if (Utils.isSplat(matchedPart)) {
StringBuilder splatParam = new StringBuilder(request.get(i));
if (!sameLength && (i == (nbrOfMatchedParts - 1))) {
for (int j = i + 1; j < nbrOfRequestParts; j++) {
splatParam.append("/");
splatParam.append(request.get(j));
}
}
splat.add(splatParam.toString());
}
}
return ImmutableList.copyOf(splat);
}
}

View File

@ -0,0 +1,214 @@
package io.jenkins.blueocean.rest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Provides functionality for modifying the response
*
* @author Per Wendel
*/
public class Response {
/**
* The logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(Response.class);
private HttpServletResponse response;
private String body;
protected Response() {
// Used by wrapper
}
Response(HttpServletResponse response) {
this.response = response;
}
/**
* Sets the status code for the
*
* @param statusCode the status code
*/
public void status(int statusCode) {
response.setStatus(statusCode);
}
/**
* Sets the content type for the response
*
* @param contentType the content type
*/
public void type(String contentType) {
response.setContentType(contentType);
}
/**
* Sets the body
*
* @param body the body
*/
public void body(String body) {
this.body = body;
}
/**
* returns the body
*
* @return the body
*/
public String body() {
return this.body;
}
/**
* @return the raw response object handed in by Jetty
*/
public HttpServletResponse raw() {
return response;
}
/**
* Trigger a browser redirect
*
* @param location Where to redirect
*/
public void redirect(String location) {
if (LOG.isDebugEnabled()) {
LOG.debug("Redirecting ({} {} to {}", "Found", HttpServletResponse.SC_FOUND, location);
}
try {
response.sendRedirect(location);
} catch (IOException ioException) {
LOG.warn("Redirect failure", ioException);
}
}
/**
* Trigger a browser redirect with specific http 3XX status code.
*
* @param location Where to redirect permanently
* @param httpStatusCode the http status code
*/
public void redirect(String location, int httpStatusCode) {
if (LOG.isDebugEnabled()) {
LOG.debug("Redirecting ({} to {}", httpStatusCode, location);
}
response.setStatus(httpStatusCode);
response.setHeader("Location", location);
response.setHeader("Connection", "close");
try {
response.sendError(httpStatusCode);
} catch (IOException e) {
LOG.warn("Exception when trying to redirect permanently", e);
}
}
/**
* Adds/Sets a response header
*
* @param header the header
* @param value the value
*/
public void header(String header, String value) {
response.addHeader(header, value);
}
/**
* Adds not persistent cookie to the response.
* Can be invoked multiple times to insert more than one cookie.
*
* @param name name of the cookie
* @param value value of the cookie
*/
public void cookie(String name, String value) {
cookie(name, value, -1, false);
}
/**
* Adds cookie to the response. Can be invoked multiple times to insert more than one cookie.
*
* @param name name of the cookie
* @param value value of the cookie
* @param maxAge max age of the cookie in seconds (negative for the not persistent cookie,
* zero - deletes the cookie)
*/
public void cookie(String name, String value, int maxAge) {
cookie(name, value, maxAge, false);
}
/**
* Adds cookie to the response. Can be invoked multiple times to insert more than one cookie.
*
* @param name name of the cookie
* @param value value of the cookie
* @param maxAge max age of the cookie in seconds (negative for the not persistent cookie, zero - deletes the cookie)
* @param secured if true : cookie will be secured
*/
public void cookie(String name, String value, int maxAge, boolean secured) {
cookie(name, value, maxAge, secured, false);
}
/**
* Adds cookie to the response. Can be invoked multiple times to insert more than one cookie.
*
* @param name name of the cookie
* @param value value of the cookie
* @param maxAge max age of the cookie in seconds (negative for the not persistent cookie, zero - deletes the cookie)
* @param secured if true : cookie will be secured
* @param httpOnly if true: cookie will be marked as http only
*/
public void cookie(String name, String value, int maxAge, boolean secured, boolean httpOnly) {
cookie("", name, value, maxAge, secured, httpOnly);
}
/**
* Adds cookie to the response. Can be invoked multiple times to insert more than one cookie.
*
* @param path path of the cookie
* @param name name of the cookie
* @param value value of the cookie
* @param maxAge max age of the cookie in seconds (negative for the not persistent cookie, zero - deletes the cookie)
* @param secured if true : cookie will be secured
*/
public void cookie(String path, String name, String value, int maxAge, boolean secured) {
cookie(path, name, value, maxAge, secured, false);
}
/**
* Adds cookie to the response. Can be invoked multiple times to insert more than one cookie.
*
* @param path path of the cookie
* @param name name of the cookie
* @param value value of the cookie
* @param maxAge max age of the cookie in seconds (negative for the not persistent cookie, zero - deletes the cookie)
* @param secured if true : cookie will be secured
* @param httpOnly if true: cookie will be marked as http only
*/
public void cookie(String path, String name, String value, int maxAge, boolean secured, boolean httpOnly) {
Cookie cookie = new Cookie(name, value);
cookie.setPath(path);
cookie.setMaxAge(maxAge);
cookie.setSecure(secured);
//TODO: Cookie.setHttpOnly() is servlet 3.0, get HttpOnly header written at Set-Cookie header somewhere else
// cookie.setHttpOnly(httpOnly);
response.addCookie(cookie);
}
/**
* Removes the cookie.
*
* @param name name of the cookie
*/
public void removeCookie(String name) {
Cookie cookie = new Cookie(name, "");
cookie.setMaxAge(0);
response.addCookie(cookie);
}
}

View File

@ -0,0 +1,133 @@
package io.jenkins.blueocean.rest;
import javax.servlet.http.HttpSession;
import java.util.Enumeration;
import java.util.Set;
import java.util.TreeSet;
/**
* Provides session information.
* Copied and modified from <a href="https://github.com/perwendel/spark">Sinatra inspired Spark</a>
*/
public class Session {
private final HttpSession session;
/**
* Creates a session with the <code>HttpSession</code>.
*
* @param session
* @throws IllegalArgumentException If the session is null.
*/
Session(HttpSession session) {
if (session == null) {
throw new IllegalArgumentException("session cannot be null");
}
this.session = session;
}
/**
* @return the raw <code>HttpSession</code> object handed in by the servlet container.
*/
public HttpSession raw() {
return session;
}
/**
* Returns the object bound with the specified name in this session, or null if no object is bound under the name.
*
* @param name a string specifying the name of the object
* @param <T> The type parameter
* @return the object with the specified name
*/
@SuppressWarnings("unchecked")
public <T> T attribute(String name) {
return (T) session.getAttribute(name);
}
/**
* Binds an object to this session, using the name specified.
*
* @param name the name to which the object is bound; cannot be null
* @param value the object to be bound
*/
public void attribute(String name, Object value) {
session.setAttribute(name, value);
}
/**
* @return an <code>Enumeration</code> of <code>String</code> objects
* containing the names of all the objects bound to this session.
*/
public Set<String> attributes() {
TreeSet<String> attributes = new TreeSet<String>();
Enumeration<String> enumeration = session.getAttributeNames();
while (enumeration.hasMoreElements()) {
attributes.add(enumeration.nextElement());
}
return attributes;
}
/**
* @return the time when this session was created, measured in milliseconds since midnight January 1, 1970 GMT.
*/
public long creationTime() {
return session.getCreationTime();
}
/**
* @return a string containing the unique identifier assigned to this session.
*/
public String id() {
return session.getId();
}
/**
* @return the last time the client sent a request associated with this session,
* as the number of milliseconds since midnight January 1, 1970 GMT, and marked
* by the time the container received the request.
*/
public long lastAccessedTime() {
return session.getLastAccessedTime();
}
/**
* @return the maximum time interval, in seconds, that the container
* will keep this session open between client accesses.
*/
public int maxInactiveInterval() {
return session.getMaxInactiveInterval();
}
/**
* Specifies the time, in seconds, between client requests the web container will invalidate this session.
*
* @param interval the interval
*/
public void maxInactiveInterval(int interval) {
session.setMaxInactiveInterval(interval);
}
/**
* Invalidates this session then unbinds any objects bound to it.
*/
public void invalidate() {
session.invalidate();
}
/**
* @return true if the client does not yet know about the session or if the client chooses not to join the session.
*/
public boolean isNew() {
return session.isNew();
}
/**
* Removes the object bound with the specified name from this session.
*
* @param name the name of the object to remove from this session
*/
public void removeAttribute(String name) {
session.removeAttribute(name);
}
}

View File

@ -0,0 +1,71 @@
package io.jenkins.blueocean.rest;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import io.jenkins.blueocean.commons.ServiceException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author Vivek Pandey
*/
public class Utils {
public static final String ALL_PATHS = "+/*paths";
public static List<String> convertRouteToList(String route) {
String[] pathArray = route.split("/");
List<String> path = new ArrayList<String>();
for (String p : pathArray) {
if (p.length() > 0) {
path.add(p);
}
}
return path;
}
public static boolean isParam(String routePart) {
return routePart.startsWith(":");
}
public static boolean isSplat(String routePart) {
return routePart.equals("*");
}
@SuppressWarnings("unchecked")
public static <T> T cast(String value, Class<T> type){
try{
if(String.class.isAssignableFrom(type)){
return (T)value;
}else if(Integer.class.isAssignableFrom(type)){
return (T)Integer.valueOf(value);
}else if(Long.class.isAssignableFrom(type)){
return (T)Long.valueOf(value);
}else if(Boolean.class.isAssignableFrom(type)){
return (T)Boolean.valueOf(value);
}else if(List.class.isAssignableFrom(type)){ //List<String>
String[] vs = value.split(",");
List<String> list = new ArrayList<>();
for(String v: vs){
list.add(v.trim());
}
return (T)ImmutableList.of(list);
}else if(Set.class.isAssignableFrom(type)){ //List<String>
String[] vs = value.split(",");
Set<String> set = new HashSet<>();
for(String v: vs){
set.add(v.trim());
}
return (T) ImmutableSet.of(set);
}else{
throw new ServiceException.UnexpectedErrorExpcetion(
String.format("Unknown type %s", type));
}
}catch (NumberFormatException e){
throw new ServiceException.BadRequestExpception(
String.format("Value %s can't be converted to type: %s", value, type));
}
}
}

View File

@ -0,0 +1,232 @@
package io.jenkins.blueocean.rest.router;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* MIME-Type Parser
*
* @author Vivek Pandey
*/
class MimeParser {
private MimeParser() {
}
/**
* Constant for no mime type
*/
public static final String NO_MIME_TYPE = "";
public static final String APPLICATION_JSON = "application/json";
/**
* Parse results container
*/
private static class ParseResults {
String type;
String subType;
// !a dictionary of all the parameters for the media range
Map<String, String> params;
@Override
public String toString() {
StringBuilder s = new StringBuilder("('" + type + "', '" + subType + "', {");
for (Map.Entry<String,String> k : params.entrySet()) {
s.append("'").append(k.getKey()).append("':'").append(k.getValue()).append("',");
}
return s.append("})").toString();
}
}
/**
* Carves up a mime-type and returns a ParseResults object
* For example, the media range 'application/xhtml;q=0.5' would get parsed
* into:
* ('application', 'xhtml', {'q', '0.5'})
*/
private static ParseResults parseMimeType(String mimeType) {
String[] parts = mimeType.split(";");
ParseResults results = new ParseResults();
results.params = new HashMap<>();
for (int i = 1; i < parts.length; ++i) {
String p = parts[i];
String[] subParts = p.split("=");
if (subParts.length == 2) {
results.params.put(subParts[0].trim(), subParts[1].trim());
}
}
String fullType = parts[0].trim();
// Java URLConnection class sends an Accept header that includes a
// single "*" - Turn it into a legal wildcard.
if (fullType.equals("*")) {
fullType = "*/*";
}
int slashIndex = fullType.indexOf('/');
if (slashIndex != -1) {
results.type = fullType.substring(0, slashIndex);
results.subType = fullType.substring(slashIndex + 1);
} else {
//If the type is invalid, attempt to turn into a wildcard
results.type = fullType;
results.subType = "*";
}
return results;
}
/**
* Carves up a media range and returns a ParseResults.
* For example, the media range 'application/*;q=0.5' would get parsed into:
* ('application', '*', {'q', '0.5'})
* In addition this function also guarantees that there is a value for 'q'
* in the pathParam dictionary, filling it in with a proper default if
* necessary.
*
* @param range
*/
private static ParseResults parseMediaRange(String range) {
ParseResults results = parseMimeType(range);
String q = results.params.get("q");
float f = toFloat(q, 1);
if (isBlank(q) || f < 0 || f > 1) {
results.params.put("q", "1");
}
return results;
}
/**
* Structure for holding a fitness/quality combo
*/
private static class FitnessAndQuality implements Comparable<FitnessAndQuality> {
int fitness;
float quality;
String mimeType; // optionally used
private FitnessAndQuality(int fitness, float quality) {
this.fitness = fitness;
this.quality = quality;
}
public int compareTo(FitnessAndQuality o) {
if (fitness == o.fitness) {
if (quality == o.quality) {
return 0;
} else {
return Float.compare(quality,o.quality) < 0 ? -1 : 1;
}
} else {
return (fitness < o.fitness) ? -1 : 1;
}
}
@Override
public boolean equals(Object obj) {
if (obj == null)
return false;
if (!(obj instanceof FitnessAndQuality))
return false;
FitnessAndQuality other = (FitnessAndQuality) obj;
return fitness == other.fitness && quality == other.quality;
}
@Override
public int hashCode() {
int hashCode = 1;
hashCode = (int) (31 * hashCode + quality);
hashCode = 31 * hashCode + fitness;
hashCode = 31 * hashCode + mimeType.hashCode();
return hashCode;
}
}
/**
* Find the best match for a given mimeType against a list of media_ranges
* that have already been parsed by MimeParse.parseMediaRange(). Returns a
* tuple of the fitness value and the value of the 'q' quality parameter of
* the best match, or (-1, 0) if no match was found. Just as for
* quality_parsed(), 'parsed_ranges' must be a list of parsed media ranges.
*
* @param mimeType
* @param parsedRanges
*/
private static FitnessAndQuality fitnessAndQualityParsed(String mimeType, Collection<ParseResults> parsedRanges) {
int bestFitness = -1;
float bestFitQ = 0;
ParseResults target = parseMediaRange(mimeType);
for (ParseResults range : parsedRanges) {
if ((target.type.equals(range.type) || range.type.equals("*") || target.type.equals("*"))
&& (target.subType.equals(range.subType) || range.subType.equals("*")
|| target.subType.equals("*"))) {
for (String k : target.params.keySet()) {
int paramMatches = 0;
if (!k.equals("q") && range.params.containsKey(k)
&& target.params.get(k).equals(range.params.get(k))) {
paramMatches++;
}
int fitness = (range.type.equals(target.type)) ? 100 : 0;
fitness += (range.subType.equals(target.subType)) ? 10 : 0;
fitness += paramMatches;
if (fitness > bestFitness) {
bestFitness = fitness;
bestFitQ = toFloat(range.params.get("q"), 0);
}
}
}
}
return new FitnessAndQuality(bestFitness, bestFitQ);
}
/**
* Finds best match
*
* @param supported the supported types
* @param header the header
* @return the best match
*/
public static String bestMatch(Collection<String> supported, String header) {
List<ParseResults> parseResults = new LinkedList<ParseResults>();
List<FitnessAndQuality> weightedMatches = new LinkedList<FitnessAndQuality>();
for (String r : header.split(",")) {
parseResults.add(parseMediaRange(r));
}
for (String s : supported) {
FitnessAndQuality fitnessAndQuality = fitnessAndQualityParsed(s, parseResults);
fitnessAndQuality.mimeType = s;
weightedMatches.add(fitnessAndQuality);
}
Collections.sort(weightedMatches);
FitnessAndQuality lastOne = weightedMatches.get(weightedMatches.size() - 1);
return Float.compare(lastOne.quality, 0) != 0 ? lastOne.mimeType : NO_MIME_TYPE;
}
private static boolean isBlank(String s) {
return s == null || "".equals(s.trim());
}
private static float toFloat(final String str, final float defaultValue) {
if (str == null) {
return defaultValue;
}
try {
return Float.parseFloat(str);
} catch (final NumberFormatException nfe) {
return defaultValue;
}
}
}

View File

@ -0,0 +1,93 @@
package io.jenkins.blueocean.rest.router;
import io.jenkins.blueocean.commons.ServiceException;
import io.jenkins.blueocean.rest.Request;
import io.jenkins.blueocean.rest.Response;
import javax.annotation.Nonnull;
/**
* Any REST resource that can handle an HTTP route gets chance to do it by implementing this interface.
*
* Inspired from <a href="https://github.com/perwendel/spark">Sinatra inspired Spark</a>
*
* @author Vivek Pandey
*/
public interface Route {
String DEFAULT_ACCEPT_TYPE = "*/*";
String DEFAULT_CONTENT_TYPE = "application/json";
/**
* Invoked when a request is made on this route's corresponding path e.g. '/hello'
*
* @param request The request object providing information about the HTTP request
* @param response The response object providing functionality for modifying the response
* @return The content to be set in the response
* @throws ServiceException if there is any exception
*/
Object handle(Request request, Response response);
/**
* Gives URL path this Route can handle. e.g. /organizations/:organization-id
*
* @return URL path
*/
@Nonnull String path();
/**
* Gives accessType this route can consume. Incoming requests's Access header will be matched against the
* Route's accessType and if it doesn't match then an error will be generated with 406 Not Acceptable error.
*
* @return MIME type the route can
*/
@Nonnull String accessType();
/**
* Content type this route produces
*
* @return content type
*/
String contentType();
abstract class RouteImpl implements Route{
private final String path;
private final String acceptType;
private final String contentType;
public RouteImpl(String path) {
this.path = path;
this.acceptType = DEFAULT_ACCEPT_TYPE;
this.contentType = DEFAULT_CONTENT_TYPE;
}
public RouteImpl(String path, String acceptType) {
this.path = path;
this.acceptType = acceptType;
this.contentType = DEFAULT_CONTENT_TYPE;
}
public RouteImpl(String path, String acceptType, String contentType) {
this.path = path;
this.acceptType = acceptType;
this.contentType = contentType;
}
@Override
public String path() {
return path;
}
@Override
public String accessType() {
return acceptType;
}
@Override
public String contentType() {
return contentType;
}
}
}

View File

@ -0,0 +1,101 @@
package io.jenkins.blueocean.rest.router;
/**
* @author Vivek Pandey
*/
import io.jenkins.blueocean.rest.Body;
import io.jenkins.blueocean.rest.HttpMethod;
import io.jenkins.blueocean.rest.Response;
import javax.servlet.http.HttpServletRequest;
/**
* Holds the parameters needed in the Before filters, Routes and After filters execution.
*/
public final class RouteContext {
/**
* Creates a RouteContext
*/
public static RouteContext create() {
return new RouteContext();
}
private RouteMatcher routeMatcher;
private HttpServletRequest httpRequest;
private String uri;
private String acceptType;
private Body body;
private Response response;
private HttpMethod httpMethod;
private RouteContext() {
// hidden
}
public RouteMatcher routeMatcher() {
return routeMatcher;
}
public RouteContext withMatcher(RouteMatcher routeMatcher) {
this.routeMatcher = routeMatcher;
return this;
}
public RouteContext withHttpRequest(HttpServletRequest httpRequest) {
this.httpRequest = httpRequest;
return this;
}
public RouteContext withAcceptType(String acceptType) {
this.acceptType = acceptType;
return this;
}
public RouteContext withBody(Body body) {
this.body = body;
return this;
}
public RouteContext withUri(String uri) {
this.uri = uri;
return this;
}
public RouteContext withResponse(Response response) {
this.response = response;
return this;
}
public RouteContext withHttpMethod(HttpMethod httpMethod) {
this.httpMethod = httpMethod;
return this;
}
public HttpServletRequest httpRequest() {
return httpRequest;
}
public String uri() {
return uri;
}
public String acceptType() {
return acceptType;
}
public Body body() {
return body;
}
public Response response() {
return response;
}
public HttpMethod httpMethod() {
return httpMethod;
}
}

View File

@ -0,0 +1,113 @@
package io.jenkins.blueocean.rest.router;
import io.jenkins.blueocean.rest.HttpMethod;
import io.jenkins.blueocean.rest.Utils;
import java.util.List;
/**
* @author Per Wendel
*/
class RouteEntry {
final HttpMethod httpMethod;
final String path;
final String acceptedType;
final Route target;
public RouteEntry(HttpMethod httpMethod, String path, String acceptedType, Route target) {
this.httpMethod = httpMethod;
this.path = path;
this.acceptedType = acceptedType;
this.target = target;
}
boolean matches(HttpMethod httpMethod, String path) {
if ((httpMethod == HttpMethod.before || httpMethod == HttpMethod.after)
&& (this.httpMethod == httpMethod)
&& this.path.equals(Utils.ALL_PATHS)) {
// Is filter and matches all
return true;
}
boolean match = false;
if (this.httpMethod == httpMethod) {
match = matchPath(path);
}
return match;
}
private boolean matchPath(String path) {
if (!this.path.endsWith("*")
&& ((path.endsWith("/") && !this.path.endsWith("/"))
|| (this.path.endsWith("/") && !path.endsWith("/")))) {
// One and not both ends with slash
return false;
}
if (this.path.equals(path)) {
// Paths are the same
return true;
}
// check pathParam
List<String> thisPathList = Utils.convertRouteToList(this.path);
List<String> pathList = Utils.convertRouteToList(path);
int thisPathSize = thisPathList.size();
int pathSize = pathList.size();
if (thisPathSize == pathSize) {
for (int i = 0; i < thisPathSize; i++) {
String thisPathPart = thisPathList.get(i);
String pathPart = pathList.get(i);
if ((i == thisPathSize - 1) && (thisPathPart.equals("*") && this.path.endsWith("*"))) {
// wildcard match
return true;
}
if ((!thisPathPart.startsWith(":"))
&& !thisPathPart.equals(pathPart)
&& !thisPathPart.equals("*")) {
return false;
}
}
// All parts matched
return true;
} else {
// Number of "path parts" not the same
// check wild card:
if (this.path.endsWith("*")) {
if (pathSize == (thisPathSize - 1) && (path.endsWith("/"))) {
// Hack for making wildcards work with trailing slash
pathList.add("");
pathList.add("");
pathSize += 2;
}
if (thisPathSize < pathSize) {
for (int i = 0; i < thisPathSize; i++) {
String thisPathPart = thisPathList.get(i);
String pathPart = pathList.get(i);
if (thisPathPart.equals("*") && (i == thisPathSize - 1) && this.path.endsWith("*")) {
// wildcard match
return true;
}
if (!thisPathPart.startsWith(":")
&& !thisPathPart.equals(pathPart)
&& !thisPathPart.equals("*")) {
return false;
}
}
// All parts matched
return true;
}
// End check wild card
}
return false;
}
}
public String toString() {
return httpMethod.name() + ", " + path + ", " + target;
}
}

View File

@ -0,0 +1,56 @@
package io.jenkins.blueocean.rest.router;
/**
*
* Copied from <a href="https://github.com/perwendel/spark">Sinatra inspired Spark</a>
*
* @author Per Wendel
* @author Vivek Pandey
*/
public class RouteMatch {
private final Route target;
private final String matchUri;
private final String requestURI;
private final String acceptType;
public RouteMatch(Route target, String matchUri, String requestUri, String acceptType) {
super();
this.target = target;
this.matchUri = matchUri;
this.requestURI = requestUri;
this.acceptType = acceptType;
}
/**
* @return the accept type
*/
public String getAcceptType() {
return acceptType;
}
/**
* @return the target
*/
public Route getTarget() {
return target;
}
/**
* @return the matchUri
*/
public String getMatchUri() {
return matchUri;
}
/**
* @return the requestUri
*/
public String getRequestURI() {
return requestURI;
}
}

View File

@ -0,0 +1,178 @@
package io.jenkins.blueocean.rest.router;
import io.jenkins.blueocean.rest.HttpMethod;
import org.apache.commons.lang.StringUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Simple route matcher that is supposed to work exactly as Sinatra's
*
* @author Per Wendel
* @author Vivek Pandey
*/
/** package */
class RouteMatcher {
private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(RouteMatcher.class);
private static final char SINGLE_QUOTE = '\'';
private final List<RouteEntry> routes = new ArrayList<>();
/**
* Parse and validates a route and adds it
*
* @param target the invocation target
*/
public void parseValidateAddRoute(HttpMethod method, Route target) {
String route = target.path();
String acceptType = target.accessType();
addRoute(method, route, acceptType, target);
}
/**
* finds target for a requested route
*
* @param httpMethod the http method
* @param path the path
* @param acceptType the accept type
* @return the target
*/
public RouteMatch findTargetForRequestedRoute(HttpMethod httpMethod, String path, String acceptType) {
List<RouteEntry> routeEntries = this.findTargetsForRequestedRoute(httpMethod, path);
RouteEntry entry = findTargetWithGivenAcceptType(routeEntries, acceptType);
return entry != null ? new RouteMatch(entry.target, entry.path, path, acceptType) : null;
}
/**
* ¨Clear all routes
*/
public void clearRoutes() {
routes.clear();
}
/**
* Removes a particular route from the collection of those that have been previously routed.
* Search for a previously established routes using the given path and HTTP method, removing
* any matches that are found.
*
* @param path the route path
* @param httpMethod the http method
* @return <tt>true</tt> if this a matching route has been previously routed
* @throws IllegalArgumentException if <tt>path</tt> is null or blank or if <tt>httpMethod</tt> is null, blank
* or an invalid HTTP method
* @since 2.2
*/
public boolean removeRoute(String path, String httpMethod) {
if (StringUtils.isEmpty(path)) {
throw new IllegalArgumentException("path cannot be null or blank");
}
if (StringUtils.isEmpty(httpMethod)) {
throw new IllegalArgumentException("httpMethod cannot be null or blank");
}
// Catches invalid input and throws IllegalArgumentException
HttpMethod method = HttpMethod.valueOf(httpMethod);
return removeRoute(method, path);
}
/**
* Removes a particular route from the collection of those that have been previously routed.
* Search for a previously established routes using the given path and removes any matches that are found.
*
* @param path the route path
* @return <tt>true</tt> if this a matching route has been previously routed
* @throws java.lang.IllegalArgumentException if <tt>path</tt> is null or blank
* @since 2.2
*/
public boolean removeRoute(String path) {
if (StringUtils.isEmpty(path)) {
throw new IllegalArgumentException("path cannot be null or blank");
}
return removeRoute((HttpMethod) null, path);
}
//////////////////////////////////////////////////
// PRIVATE METHODS
//////////////////////////////////////////////////
private void addRoute(HttpMethod method, String url, String acceptedType, Route target) {
RouteEntry entry = new RouteEntry(method, url, acceptedType, target);
LOG.debug("Adds route: " + entry);
// Adds to end of list
routes.add(entry);
}
//can be cached? I don't think so.
private Map<String, RouteEntry> getAcceptedMimeTypes(List<RouteEntry> routes) {
Map<String, RouteEntry> acceptedTypes = new HashMap<>();
for (RouteEntry routeEntry : routes) {
if (!acceptedTypes.containsKey(routeEntry.acceptedType)) {
acceptedTypes.put(routeEntry.acceptedType, routeEntry);
}
}
return acceptedTypes;
}
private boolean routeWithGivenAcceptType(String bestMatch) {
return !MimeParser.NO_MIME_TYPE.equals(bestMatch);
}
private List<RouteEntry> findTargetsForRequestedRoute(HttpMethod httpMethod, String path) {
List<RouteEntry> matchSet = new ArrayList<RouteEntry>();
for (RouteEntry entry : routes) {
if (entry.matches(httpMethod, path)) {
matchSet.add(entry);
}
}
return matchSet;
}
private RouteEntry findTargetWithGivenAcceptType(List<RouteEntry> routeMatches, String acceptType) {
if (acceptType != null && routeMatches.size() > 0) {
Map<String, RouteEntry> acceptedMimeTypes = getAcceptedMimeTypes(routeMatches);
String bestMatch = MimeParser.bestMatch(acceptedMimeTypes.keySet(), acceptType);
if (routeWithGivenAcceptType(bestMatch)) {
return acceptedMimeTypes.get(bestMatch);
} else {
return null;
}
} else {
if (routeMatches.size() > 0) {
return routeMatches.get(0);
}
}
return null;
}
private boolean removeRoute(HttpMethod httpMethod, String path) {
List<RouteEntry> forRemoval = new ArrayList<>();
for (RouteEntry routeEntry : routes) {
HttpMethod httpMethodToMatch = httpMethod;
if (httpMethod == null) {
// Use the routeEntry's HTTP method if none was given, so that only path is used to match.
httpMethodToMatch = routeEntry.httpMethod;
}
if (routeEntry.matches(httpMethodToMatch, path)) {
LOG.debug("Removing path {}", path, httpMethod == null ? "" : " with HTTP method " + httpMethod);
forRemoval.add(routeEntry);
}
}
return routes.removeAll(forRemoval);
}
}

View File

@ -0,0 +1,103 @@
package io.jenkins.blueocean.rest.router;
import io.jenkins.blueocean.commons.ServiceException;
import io.jenkins.blueocean.rest.HttpMethod;
import io.jenkins.blueocean.rest.Request;
import java.util.ArrayList;
import java.util.List;
/**
* @author Vivek Pandey
*/
public class Router {
private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(RouteMatcher.class);
private static final char SINGLE_QUOTE = '\'';
private final RouteMatcher routeMatcher = new RouteMatcher();
private final List<RouteEntry> routes = new ArrayList<>();
private Router() {
}
public static final Router INSTANCE = new Router();
/**
* Adds a route
*
* @param httpMethod the HTTP method
* @param route the route implementation
*/
public void addRoute(HttpMethod httpMethod, Route route){
routeMatcher.parseValidateAddRoute(httpMethod, route);
}
/**
* Adds a filter
*
* @param httpMethod the HTTP method
* @param filter the route implementation
*/
public void addFilter(HttpMethod httpMethod, Route filter){
routeMatcher.parseValidateAddRoute(httpMethod, filter);
}
public static void get(Route route){
INSTANCE.addRoute(HttpMethod.get, route);
}
public static void post(Route route){
INSTANCE.addRoute(HttpMethod.post, route);
}
public static void put(Route route){
INSTANCE.addRoute(HttpMethod.put, route);
}
public static void patch(Route route){
INSTANCE.addRoute(HttpMethod.patch, route);
}
public static void head(Route route){
INSTANCE.addRoute(HttpMethod.head, route);
}
public RouteMatcher routeMatcher(){
return routeMatcher;
}
public static void execute(RouteContext context){
Object content=null;
RouteMatch match = context.routeMatcher().findTargetForRequestedRoute(context.httpMethod(), context.uri(), context.acceptType());
Route target = null;
if (match != null) {
target = match.getTarget();
} else if (context.httpMethod() == HttpMethod.head && context.body().notSet()) {
// See if get is mapped to provide default head mapping
match = context.routeMatcher().findTargetForRequestedRoute(HttpMethod.get, context.uri(), context.acceptType());
content = match != null ? "" : null;
}
if (target == null) {
throw new ServiceException.NotFoundException(
String.format("No route found for requested resource %s with HTTP method %s",
context.uri(), context.httpMethod()));
}
Request request = new Request(match, context.httpRequest());
Object result = target.handle(request, context.response());;
if (result != null) {
content = result;
}
context.body().set(content);
}
}

View File

@ -0,0 +1,4 @@
<?jelly escape-by-default='true'?>
<div>
This plugin is a part of Blue Ocean UI
</div>

View File

@ -2,12 +2,14 @@ package io.jenkins.blueocean;
import com.google.inject.Inject;
import hudson.Extension;
import hudson.ExtensionList;
import io.jenkins.blueocean.api.profile.GetUserDetailsRequest;
import io.jenkins.blueocean.api.profile.ProfileService;
import io.jenkins.blueocean.config.ApplicationConfig;
import io.jenkins.blueocean.security.Cookies;
import io.jenkins.blueocean.security.Identity;
import io.jenkins.blueocean.security.LoginAction;
import jenkins.model.Jenkins;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.Stapler;
@ -47,4 +49,18 @@ public class BlueOceanUI {
Identity identity = (Identity)Stapler.getCurrentRequest().getUserPrincipal();
return profiles.getUserDetails(identity, GetUserDetailsRequest.byUserId(identity.user)).userDetails.email;
}
public Object getRest(){
try {
ExtensionList extensions = Jenkins.getActiveInstance().getExtensionList("io.jenkins.blueocean.rest.ApiHead");
if(extensions.size() > 0){
return extensions.get(0);
}else{
return HttpResponses.notFound();
}
} catch (ClassNotFoundException e) {
return HttpResponses.notFound();
}
}
}

View File

@ -1,3 +1,4 @@
<?jelly escape-by-default='true'?>
<div>
Blue Ocean core
</div>
</div>

16
embedded-driver/README.md Normal file
View File

@ -0,0 +1,16 @@
# Run embedded-driver
cd embedded-driver
mvn hpi:run
This will launch jenkins with BO plugin.
### BlueOcean UI is available at:
http://localhost:8080/jenkins/bo
> Jenkins classic UI is available at http://localhost:8080/jenkins/
### BlueOcean rest API base URL is:
http://localhost:8080/jenkins/bo/rest

View File

@ -14,30 +14,56 @@
<name>BlueOcean :: BlueOcean Embedded Service API Driver</name>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>blueocean-commons</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>blueocean-security-api</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>profile-service-api</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>pipeline-service-api</artifactId>
</dependency>
<dependency>
<groupId>io.jenkins.blueocean</groupId>
<artifactId>authentication-service-api</artifactId>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>mailer</artifactId>
</dependency>
</dependencies>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>blueocean-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>blueocean-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>blueocean-security-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>profile-service-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>pipeline-service-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>blueocean-rest</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>blueocean-rest</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>mailer</artifactId>
<version>1.13</version>
</dependency>
<dependency>
<groupId>com.jayway.restassured</groupId>
<artifactId>rest-assured</artifactId>
<version>2.8.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jenkins.blueocean</groupId>
<artifactId>authentication-service-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -10,10 +10,12 @@ import jenkins.model.Jenkins;
*/
public abstract class AbstractEmbeddedService {
protected final Jenkins jenkins = Jenkins.getActiveInstance();
protected Jenkins getJenkins(){
return Jenkins.getActiveInstance();
};
protected void validateOrganization(String organization){
if (!organization.equals(Jenkins.getActiveInstance().getDisplayName())) {
if (!organization.equals(Jenkins.getActiveInstance().getDisplayName().toLowerCase())) {
throw new ServiceException.UnprocessableEntityException(String.format("Organization %s not found",
organization));
}

View File

@ -1,7 +1,8 @@
package io.jenkins.blueocean;
package io.jenkins.blueocean.service.embedded;
import hudson.Extension;
import hudson.model.RootAction;
import io.jenkins.blueocean.BlueOceanUI;
import org.kohsuke.stapler.StaplerProxy;
import javax.inject.Inject;

View File

@ -1,6 +1,8 @@
package io.jenkins.blueocean.service.embedded;
import com.google.common.collect.ImmutableMap;
import hudson.Extension;
import hudson.model.Job;
import hudson.model.Project;
import hudson.model.Result;
import hudson.util.RunList;
@ -13,11 +15,10 @@ import io.jenkins.blueocean.api.pipeline.GetPipelineResponse;
import io.jenkins.blueocean.api.pipeline.GetPipelineRunRequest;
import io.jenkins.blueocean.api.pipeline.GetPipelineRunResponse;
import io.jenkins.blueocean.api.pipeline.PipelineService;
import io.jenkins.blueocean.api.pipeline.model.JobResult;
import io.jenkins.blueocean.api.pipeline.model.Pipeline;
import io.jenkins.blueocean.api.pipeline.model.Run;
import io.jenkins.blueocean.security.Identity;
import io.jenkins.blueocean.commons.ServiceException;
import io.jenkins.blueocean.security.Identity;
import javax.annotation.Nonnull;
import java.util.ArrayList;
@ -38,7 +39,7 @@ public class EmbeddedPipelineService extends AbstractEmbeddedService implements
public GetPipelineResponse getPipeline(@Nonnull Identity identity, @Nonnull GetPipelineRequest pipelineRequest) {
validateOrganization(pipelineRequest.organization);
List<Project> projects = jenkins.getAllItems(Project.class);
List<Project> projects = getJenkins().getAllItems(Project.class);
for (Project project : projects) {
if (project.getName().equals(pipelineRequest.pipeline)) {
return new GetPipelineResponse(new Pipeline(pipelineRequest.organization, project.getName(),
@ -54,15 +55,15 @@ public class EmbeddedPipelineService extends AbstractEmbeddedService implements
public FindPipelinesResponse findPipelines(@Nonnull Identity identity, @Nonnull FindPipelinesRequest findPipelinesRequest) {
validateOrganization(findPipelinesRequest.organization);
List<Project> projects = jenkins.getAllItems(Project.class);
List<Job> projects = getJenkins().getAllItems(Job.class);
List<Pipeline> pipelines = new ArrayList<Pipeline>();
for (Project project : projects) {
for (Job project : projects) {
if (findPipelinesRequest.pipeline != null &&
project.getName().contains(findPipelinesRequest.pipeline)) {
pipelines.add(new Pipeline(findPipelinesRequest.organization, project.getName(),
Collections.EMPTY_LIST));
!project.getName().contains(findPipelinesRequest.pipeline)) {
continue;
}
pipelines.add(new Pipeline(findPipelinesRequest.organization, project.getName(),
Collections.EMPTY_LIST));
}
return new FindPipelinesResponse(pipelines);
}
@ -72,18 +73,34 @@ public class EmbeddedPipelineService extends AbstractEmbeddedService implements
public GetPipelineRunResponse getPipelineRun(@Nonnull Identity identity, @Nonnull GetPipelineRunRequest request) {
validateOrganization(request.organization);
List<Project> projects = jenkins.getAllItems(Project.class);
for (Project p : projects) {
List<Job> projects = getJenkins().getAllItems(Job.class);
for (Job p : projects) {
if (!p.getName().equals(request.pipeline)) {
continue;
}
RunList<? extends hudson.model.Run> runList = p.getBuilds();
hudson.model.Run run = null;
if(request.run != null) {
for (hudson.model.Run r : runList) {
if (r.getId().equals(request.run)) {
run = r;
break;
}
}
if(run == null){
throw new ServiceException.NotFoundException(
String.format("Run %s not found in organization %s and pipeline %s",
request.run, request.organization, request.pipeline));
}
}else{
run = runList.getLastBuild();
}
return new GetPipelineRunResponse(
createBoRun(runList.getLastBuild(), request.organization, request.pipeline));
createBoRun(run.getClass().getSimpleName(),run, request.organization, request.pipeline));
}
throw new ServiceException.NotFoundException(String.format("No run found for organization %s, pipeline: %s",
request.organization, request.pipeline));
throw new ServiceException.NotFoundException(String.format("Run id %s not found for organization %s, pipeline: %s",
request.run, request.organization, request.pipeline));
}
@ -93,8 +110,8 @@ public class EmbeddedPipelineService extends AbstractEmbeddedService implements
validateOrganization(request.organization);
List<Run> runs = new ArrayList<Run>();
List<Project> projects = jenkins.getAllItems(Project.class);
for (Project p : projects) {
List<Job> projects = getJenkins().getAllItems(Job.class);
for (Job p : projects) {
if (request.pipeline != null && !p.getName().equals(request.pipeline)) {
continue;
}
@ -102,7 +119,7 @@ public class EmbeddedPipelineService extends AbstractEmbeddedService implements
if (request.latestOnly) {
hudson.model.Run r = p.getLastBuild();
if(r != null) {
Run run = createBoRun(r, request.organization, p.getName());
Run run = createBoRun(r.getClass().getSimpleName(),r, request.organization, p.getName());
runs.add(run);
}else{
return new FindPipelineRunsResponse(runs,null, null);
@ -112,14 +129,15 @@ public class EmbeddedPipelineService extends AbstractEmbeddedService implements
Iterator<? extends hudson.model.Run> iterator = runList.iterator();
while (iterator.hasNext()) {
runs.add(createBoRun(iterator.next(), request.organization, p.getName()));
hudson.model.Run r = iterator.next();
runs.add(createBoRun(r.getClass().getSimpleName(),r, request.organization, p.getName()));
}
}
}
return new FindPipelineRunsResponse(runs, null, null);
}
private Run createBoRun(hudson.model.Run r, String organization, String pipeline) {
private Run createBoRun(String buildClass, hudson.model.Run r, String organization, String pipeline) {
Date endTime = null;
if (!r.isBuilding()) {
endTime = new Date(r.getStartTimeInMillis() + r.getDuration());
@ -132,8 +150,7 @@ public class EmbeddedPipelineService extends AbstractEmbeddedService implements
.durationInMillis(r.getDuration())
.status(getStatusFromJenkinsRun(r))
.runSummary(r.getBuildStatusSummary().message)
//TODO: need to determine how to check if it's pipeline build
.result(new JobResult())
.result(new io.jenkins.blueocean.api.pipeline.model.Result(buildClass, ImmutableMap.<String, Object>of()))
.build();
}

View File

@ -74,7 +74,7 @@ public class EmbeddedProfileService extends AbstractEmbeddedService implements P
@Override
public GetOrganizationResponse getOrganization(@Nonnull Identity identity, @Nonnull GetOrganizationRequest request) {
validateOrganization(request.name);
return new GetOrganizationResponse(new Organization(jenkins.getDisplayName()));
return new GetOrganizationResponse(new Organization(getJenkins().getDisplayName().toLowerCase()));
}
@Nonnull

View File

@ -0,0 +1,176 @@
package io.jenkins.blueocean.service.embedded;
import com.google.common.collect.ImmutableMap;
import com.jayway.restassured.RestAssured;
import com.jayway.restassured.response.Response;
import com.jayway.restassured.response.ValidatableResponse;
import hudson.model.FreeStyleBuild;
import hudson.model.FreeStyleProject;
import hudson.tasks.Shell;
import io.jenkins.blueocean.commons.JsonConverter;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.ExecutionException;
/**
* @author Vivek Pandey
*/
public class EmbeddedPipelineServiceRestTest{
@Rule
public JenkinsRule j = new JenkinsRule();
@Before
public void before() {
RestAssured.baseURI = j.jenkins.getRootUrl()+"bo/rest";
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}
@Test
public void getPipelineTest() throws IOException {
j.createFreeStyleProject("pipeline1");
RestAssured.given().log().all().get("/organizations/jenkins/pipelines/pipeline1")
.then().log().all()
.statusCode(200)
.body("pipeline.organization", Matchers.equalTo("jenkins"))
.body("pipeline.name", Matchers.equalTo("pipeline1"));
}
@Test
public void findPipelinesTest() throws IOException {
FreeStyleProject p1 = j.createFreeStyleProject("pipeline2");
FreeStyleProject p2 = j.createFreeStyleProject("pipeline3");
RestAssured.given().log().all().get("/search?q=type:pipeline;organization:jenkins")
.then().log().all()
.statusCode(200)
.body("pipelines[0].organization", Matchers.equalTo("jenkins"))
.body("pipelines[0].name", Matchers.equalTo(p1.getName()))
.body("pipelines[1].organization", Matchers.equalTo("jenkins"))
.body("pipelines[1].name", Matchers.equalTo(p2.getName()));
}
@Test
public void getPipelineRunTest() throws Exception {
FreeStyleProject p = j.createFreeStyleProject("pipeline4");
p.getBuildersList().add(new Shell("echo hello!\nsleep 1"));
FreeStyleBuild b = p.scheduleBuild2(0).get();
j.assertBuildStatusSuccess(b);
RestAssured.given().log().all().get("/organizations/jenkins/pipelines/pipeline4/runs/"+b.getId())
.then().log().all()
.statusCode(200)
.body("run.id", Matchers.equalTo(b.getId()))
.body("run.pipeline", Matchers.equalTo(p.getName()))
.body("run.pipeline", Matchers.equalTo(p.getName()))
.body("run.organization", Matchers.equalTo("jenkins"))
.body("run.startTime", Matchers.equalTo(
new SimpleDateFormat(JsonConverter.DATE_FORMAT_STRING).format(new Date(b.getStartTimeInMillis()))));
}
@Test
public void getPipelineRunLatestTest() throws Exception {
FreeStyleProject p = j.createFreeStyleProject("pipeline5");
p.getBuildersList().add(new Shell("echo hello!\nsleep 1"));
FreeStyleBuild b = p.scheduleBuild2(0).get();
j.assertBuildStatusSuccess(b);
RestAssured.given().log().all().get("/organizations/jenkins/pipelines/pipeline5/runs/?latestOnly=true")
.then().log().all()
.statusCode(200)
.body("runs[0].id", Matchers.equalTo(b.getId()))
.body("runs[0].pipeline", Matchers.equalTo(p.getName()))
.body("runs[0].pipeline", Matchers.equalTo(p.getName()))
.body("runs[0].organization", Matchers.equalTo("jenkins"))
.body("runs[0].startTime", Matchers.equalTo(
new SimpleDateFormat(JsonConverter.DATE_FORMAT_STRING).format(new Date(b.getStartTimeInMillis()))));
}
@Test
public void getPipelineRunsTest() throws Exception {
FreeStyleProject p = j.createFreeStyleProject("pipeline6");
p.getBuildersList().add(new Shell("echo hello!\nsleep 1"));
FreeStyleBuild b = p.scheduleBuild2(0).get();
j.assertBuildStatusSuccess(b);
System.out.println(new SimpleDateFormat(JsonConverter.DATE_FORMAT_STRING).format(new Date(b.getStartTimeInMillis())));
RestAssured.given().log().all().get("/organizations/jenkins/pipelines/pipeline6/runs")
.then().log().all()
.statusCode(200)
.body("runs[0].id", Matchers.equalTo(b.getId()))
.body("runs[0].pipeline", Matchers.equalTo(p.getName()))
.body("runs[0].organization", Matchers.equalTo("jenkins"))
.body("runs[0].startTime", Matchers.equalTo(
new SimpleDateFormat(JsonConverter.DATE_FORMAT_STRING).format(new Date(b.getStartTimeInMillis()))));
}
@Test
public void findPipelineRunsForAPipelineTest() throws Exception {
FreeStyleProject p1 = j.createFreeStyleProject("pipeline1");
FreeStyleProject p2 = j.createFreeStyleProject("pipeline2");
p1.getBuildersList().add(new Shell("echo hello!\nsleep 1"));
p2.getBuildersList().add(new Shell("echo hello!\nsleep 1"));
Stack<FreeStyleBuild> builds = new Stack<FreeStyleBuild>();
FreeStyleBuild b11 = p1.scheduleBuild2(0).get();
FreeStyleBuild b12 = p1.scheduleBuild2(0).get();
builds.push(b11);
builds.push(b12);
j.assertBuildStatusSuccess(b11);
j.assertBuildStatusSuccess(b12);
ValidatableResponse response = RestAssured.given().log().all().get("/search?q=type:run;organization:jenkins;pipeline=pipeline1")
.then().log().all()
.statusCode(200);
for (int i = 0; i < builds.size(); i++) {
FreeStyleBuild b = builds.pop();
response.body(String.format("runs[%s].id",i), Matchers.equalTo(b.getId()))
.body(String.format("runs[%s].pipeline",i), Matchers.equalTo(b.getParent().getName()))
.body(String.format("runs[%s].organization",i), Matchers.equalTo("jenkins"))
.body(String.format("runs[%s].startTime",i), Matchers.equalTo(
new SimpleDateFormat(JsonConverter.DATE_FORMAT_STRING).format(new Date(b.getStartTimeInMillis()))));
}
}
@Test
public void findPipelineRunsForAllPipelineTest() throws IOException, ExecutionException, InterruptedException {
FreeStyleProject p1 = j.createFreeStyleProject("pipeline11");
FreeStyleProject p2 = j.createFreeStyleProject("pipeline22");
p1.getBuildersList().add(new Shell("echo hello!\nsleep 1"));
p2.getBuildersList().add(new Shell("echo hello!\nsleep 1"));
Stack<FreeStyleBuild> p1builds = new Stack<FreeStyleBuild>();
p1builds.push(p1.scheduleBuild2(0).get());
p1builds.push(p1.scheduleBuild2(0).get());
Stack<FreeStyleBuild> p2builds = new Stack<FreeStyleBuild>();
p2builds.push(p2.scheduleBuild2(0).get());
p2builds.push(p2.scheduleBuild2(0).get());
Map<String, Stack<FreeStyleBuild>> buildMap = ImmutableMap.of(p1.getName(), p1builds, p2.getName(), p2builds);
Response r = RestAssured.given().log().all().get("/search?q=type:run;organization:jenkins");
ValidatableResponse response = r
.then().log().all()
.statusCode(200);
for (int i = 0; i < 4; i++) {
String pipeline = r.path(String.format("runs[%s].pipeline",i));
FreeStyleBuild b = buildMap.get(pipeline).pop();
response.body(String.format("runs[%s].id",i), Matchers.equalTo(b.getId()))
.body(String.format("runs[%s].pipeline",i), Matchers.equalTo(pipeline))
.body(String.format("runs[%s].organization",i), Matchers.equalTo("jenkins"))
.body(String.format("runs[%s].startTime",i), Matchers.equalTo(
new SimpleDateFormat(JsonConverter.DATE_FORMAT_STRING).format(new Date(b.getStartTimeInMillis()))));
}
}
}

View File

@ -1,6 +1,7 @@
package io.jenkins.blueocean.service.embedded;
import com.google.common.collect.ImmutableMap;
import com.jayway.restassured.RestAssured;
import hudson.model.FreeStyleBuild;
import hudson.model.FreeStyleProject;
import hudson.tasks.Shell;
@ -13,8 +14,8 @@ import io.jenkins.blueocean.api.pipeline.GetPipelineResponse;
import io.jenkins.blueocean.api.pipeline.GetPipelineRunRequest;
import io.jenkins.blueocean.api.pipeline.GetPipelineRunResponse;
import io.jenkins.blueocean.api.pipeline.PipelineService;
import io.jenkins.blueocean.security.Identity;
import io.jenkins.blueocean.commons.JsonConverter;
import io.jenkins.blueocean.security.Identity;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
@ -38,38 +39,42 @@ public class EmbeddedPipelineServiceTest {
public JenkinsRule j = new JenkinsRule();
@Before
public void before(){
public void before() {
List<PipelineService> PipelineService = j.jenkins.getExtensionList(PipelineService.class);
Assert.assertTrue(PipelineService.size() == 1);
this.pipelineService = PipelineService.get(0);
RestAssured.baseURI = j.jenkins.getRootUrl()+"rest";
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}
@Test
public void getPipelineTest() throws IOException {
j.createFreeStyleProject("pipeline1");
GetPipelineResponse response = pipelineService.getPipeline(Identity.ANONYMOUS,
new GetPipelineRequest("Jenkins", "pipeline1"));
new GetPipelineRequest("jenkins", "pipeline1"));
Assert.assertNotNull(response);
Assert.assertNotNull(response.pipeline);
Assert.assertEquals("pipeline1", response.pipeline.name);
Assert.assertEquals("Jenkins", response.pipeline.organization);
Assert.assertEquals("jenkins", response.pipeline.organization);
}
@Test
public void findPipelinesTest() throws IOException {
j.createFreeStyleProject("pipeline1");
j.createFreeStyleProject("pipeline2");
FindPipelinesResponse response = pipelineService.findPipelines(Identity.ANONYMOUS,
new FindPipelinesRequest("Jenkins", "pipeline"));
new FindPipelinesRequest("jenkins", "pipeline"));
Assert.assertNotNull(response);
Assert.assertEquals(2, response.pipelines.size());
Assert.assertEquals("pipeline1", response.pipelines.get(0).name);
Assert.assertEquals("Jenkins", response.pipelines.get(0).organization);
Assert.assertEquals("jenkins", response.pipelines.get(0).organization);
Assert.assertEquals("pipeline2", response.pipelines.get(1).name);
Assert.assertEquals("Jenkins", response.pipelines.get(1).organization);
Assert.assertEquals("jenkins", response.pipelines.get(1).organization);
}
@ -79,12 +84,12 @@ public class EmbeddedPipelineServiceTest {
p.getBuildersList().add(new Shell("echo hello!\nsleep 1"));
FreeStyleBuild b = p.scheduleBuild2(0).get();
GetPipelineRunResponse response = pipelineService.getPipelineRun(Identity.ANONYMOUS,
new GetPipelineRunRequest("Jenkins", "pipeline1"));
new GetPipelineRunRequest("jenkins", "pipeline1", null));
System.out.println(JsonConverter.toJson(response));
Assert.assertNotNull(response);
Assert.assertNotNull(response.run);
Assert.assertEquals(b.getId(), response.run.id);
Assert.assertEquals("Jenkins", response.run.organization);
Assert.assertEquals("jenkins", response.run.organization);
Assert.assertEquals("pipeline1", response.run.pipeline);
Assert.assertEquals(b.getStartTimeInMillis(), response.run.startTime.getTime());
}
@ -101,15 +106,15 @@ public class EmbeddedPipelineServiceTest {
builds.push(p1.scheduleBuild2(0).get());
FindPipelineRunsResponse response = pipelineService.findPipelineRuns(Identity.ANONYMOUS,
new FindPipelineRunsRequest("Jenkins", "pipeline1", false, Collections.EMPTY_LIST, null, null));
new FindPipelineRunsRequest("jenkins", "pipeline1", false, Collections.EMPTY_LIST, null, null));
System.out.println("Response: "+JsonConverter.toJson(response));
System.out.println("Response: " + JsonConverter.toJson(response));
Assert.assertNotNull(response);
Assert.assertEquals(builds.size(), response.runs.size());
for(int i = 0;i < response.runs.size(); i++){
for (int i = 0; i < response.runs.size(); i++) {
FreeStyleBuild b = builds.pop();
Assert.assertEquals(b.getId(), response.runs.get(i).id);
Assert.assertEquals("Jenkins", response.runs.get(i).organization);
Assert.assertEquals("jenkins", response.runs.get(i).organization);
Assert.assertEquals("pipeline1", response.runs.get(i).pipeline);
Assert.assertEquals(b.getStartTimeInMillis(), response.runs.get(i).startTime.getTime());
}
@ -131,22 +136,22 @@ public class EmbeddedPipelineServiceTest {
Map<String, Stack<FreeStyleBuild>> buildMap = ImmutableMap.of(p1.getName(), p1builds, p2.getName(), p2builds);
FindPipelineRunsResponse response = pipelineService.findPipelineRuns(Identity.ANONYMOUS,
new FindPipelineRunsRequest("Jenkins", null, false, Collections.EMPTY_LIST, null, null));
new FindPipelineRunsRequest("jenkins", null, false, Collections.EMPTY_LIST, null, null));
System.out.println("Response: "+JsonConverter.toJson(response));
System.out.println("Response: " + JsonConverter.toJson(response));
Assert.assertNotNull(response);
Assert.assertEquals(p1builds.size() + p2builds.size(), response.runs.size());
for(int i = 0;i < response.runs.size(); i++){
for (int i = 0; i < response.runs.size(); i++) {
FreeStyleBuild b = buildMap.get(response.runs.get(i).pipeline).pop();
Assert.assertEquals(b.getId(), response.runs.get(i).id);
Assert.assertEquals("Jenkins", response.runs.get(i).organization);
Assert.assertEquals("jenkins", response.runs.get(i).organization);
Assert.assertEquals(b.getProject().getName(), response.runs.get(i).pipeline);
Assert.assertEquals(b.getStartTimeInMillis(), response.runs.get(i).startTime.getTime());
}
}
public void getPipelineRunForWorkflow(){
public void getPipelineRunForWorkflow() {
// WorkflowJob job = j.jenkins.createProject(WorkflowJob.class, "Noddy Job");
//
// String script = "node {" +

View File

@ -0,0 +1,76 @@
package io.jenkins.blueocean.service.embedded;
import com.google.common.collect.ImmutableList;
import com.jayway.restassured.RestAssured;
import com.jayway.restassured.response.Response;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import java.util.List;
/**
* @author Vivek Pandey
*/
public class EmbeddedProfileServiceRestTest {
@Rule
public JenkinsRule j = new JenkinsRule();
@Before
public void before() {
RestAssured.baseURI = j.jenkins.getRootUrl()+"bo/rest";
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}
@Test
public void getUserTest() throws Exception {
RestAssured.given().log().all().get("/users/"+j.jenkins.getUser("SYSTEM").getId())
.then().log().all()
.statusCode(200)
.body("user.id", Matchers.equalTo(j.jenkins.getUser("SYSTEM").getId()))
.body("user.name", Matchers.equalTo(j.jenkins.getUser("SYSTEM").getFullName()));
}
@Test
public void getUserDetailsTest() throws Exception {
hudson.model.User user = j.jenkins.getUser("alice");
RestAssured.given().log().all().get("/users/"+user.getId()+"?details=true")
.then().log().all()
.statusCode(200)
.body("user.id", Matchers.equalTo(user.getId()))
.body("user.name", Matchers.equalTo(user.getFullName()))
.body("user.email", Matchers.equalTo("none"));
}
@Test
public void getOrganizationTest(){
RestAssured.given().log().all().get("/organizations/jenkins")
.then().log().all()
.statusCode(200)
.body("organization.name", Matchers.equalTo("jenkins"));
}
@Test
public void FindUsersTest() throws Exception {
List<String> names = ImmutableList.of("alice", "bob");
j.jenkins.getUser(names.get(0));
j.jenkins.getUser(names.get(1));
Response response = RestAssured.given().log().all().get("/search?q=type:user;organization:jenkins");
response.then().log().all().statusCode(200);
Assert.assertTrue(names.contains((String)response.path("users[0].id")));
Assert.assertTrue(names.contains((String)response.path("users[0].name")));
Assert.assertTrue(names.contains((String)response.path("users[1].id")));
Assert.assertTrue(names.contains((String)response.path("users[1].name")));
}
}

View File

@ -12,7 +12,6 @@ import io.jenkins.blueocean.api.profile.ProfileService;
import io.jenkins.blueocean.api.profile.model.User;
import io.jenkins.blueocean.commons.JsonConverter;
import io.jenkins.blueocean.security.Identity;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
@ -28,6 +27,10 @@ public class EmbeddedProfileServiceTest {
private ProfileService profileService;
@Rule
public JenkinsRule j = new JenkinsRule();
@Before
public void before(){
List<ProfileService> profileServices = j.jenkins.getExtensionList(ProfileService.class);
@ -35,13 +38,6 @@ public class EmbeddedProfileServiceTest {
this.profileService = profileServices.get(0);
}
@After
public void after(){
}
@Rule
public JenkinsRule j = new JenkinsRule();
@Test
public void getUserTest() throws Exception {
@ -51,9 +47,6 @@ public class EmbeddedProfileServiceTest {
Assert.assertNotNull(response.user);
Assert.assertEquals(response.user.id, "SYSTEM");
Assert.assertEquals(response.user.fullName, "SYSTEM");
}
@Test
@ -69,9 +62,9 @@ public class EmbeddedProfileServiceTest {
@Test
public void getOrganizationTest(){
GetOrganizationResponse response = profileService.getOrganization(new Identity("alice"),
new GetOrganizationRequest("Jenkins"));
new GetOrganizationRequest("jenkins"));
Assert.assertNotNull(response.organization);
Assert.assertEquals(response.organization.name, "Jenkins");
Assert.assertEquals(response.organization.name, "jenkins");
}
@Test
@ -80,7 +73,7 @@ public class EmbeddedProfileServiceTest {
j.jenkins.getUser(names[0]);
j.jenkins.getUser(names[1]);
FindUsersResponse response = profileService.findUsers(new Identity("alice"), new FindUsersRequest("Jenkins", null, null));
FindUsersResponse response = profileService.findUsers(new Identity("alice"), new FindUsersRequest("jenkins", null, null));
System.out.println(JsonConverter.toJson(response));
Assert.assertTrue(response.users.size() == 2);

View File

@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import io.jenkins.blueocean.security.Identity;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* Request for {@link PipelineService#getPipelineRun(Identity, GetPipelineRunRequest)}
@ -17,10 +18,15 @@ public final class GetPipelineRunRequest {
@JsonProperty("pipeline")
public final String pipeline;
/** run is run id, if null latest run is expected */
@JsonProperty("run")
public final String run;
public GetPipelineRunRequest(@Nonnull @JsonProperty("organization") String organization,
@Nonnull @JsonProperty("pipeline") String pipeline) {
@Nonnull @JsonProperty("pipeline") String pipeline,
@Nullable @JsonProperty("run") String run) {
this.organization = organization;
this.pipeline = pipeline;
this.run = run;
}
}

View File

@ -1,15 +1,32 @@
package io.jenkins.blueocean.api.pipeline.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.collect.ImmutableMap;
import javax.annotation.Nonnull;
import java.util.Map;
/**
* Describes Job result
*
* @author Vivek Pandey
*/
public class JobResult implements Result {
@Override
public Type getType() {
return Type.JOB;
public class JobResult{
@JsonProperty("data")
private final Map<String, String> jobResultData;
public JobResult(@Nonnull @JsonProperty("data") Map<String, String> jobResultData) {
this.jobResultData = jobResultData;
}
//TODO: to be filled later based on how BO UI evolves
public String getType() {
return "job";
}
public Map<String,String> getData() {
return ImmutableMap.copyOf(jobResultData);
}
}

View File

@ -1,15 +1,31 @@
package io.jenkins.blueocean.api.pipeline.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.annotation.Nonnull;
import java.util.Map;
/**
* Abstract to describe Pipeline/workflow build result
*
* @author Vivek Pandey
*/
public class PipelineResult implements Result {
@Override
public Type getType() {
public class PipelineResult{
@JsonProperty("data")
private final Map<String, Object> pipelineResultData;
public PipelineResult(@Nonnull @JsonProperty("data") Map<String, Object> data) {
this.pipelineResultData = data;
}
public String getType() {
return null;
}
//TODO: to be filled later based on how BO UI evolves
public Map<String, Object> getData() {
return pipelineResultData;
}
}

View File

@ -1,8 +1,10 @@
package io.jenkins.blueocean.api.pipeline.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.collect.ImmutableMap;
import javax.annotation.Nonnull;
import java.util.Map;
/**
* Marker interface that describes build result
@ -12,26 +14,15 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
* @see JobResult
* @see Run
*/
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "resultType")
@JsonSubTypes({
@JsonSubTypes.Type(value = JobResult.class, name = "JOB"),
@JsonSubTypes.Type(value = PipelineResult.class, name = "PIPELINE") })
public interface Result {
public class Result{
@JsonProperty("type")
public final String type;
/** Result types, must be all names defined inside JsonSubTypes annotation */
enum Type {JOB, PIPELINE};
@JsonProperty("data")
public final Map<String, ?> data;
/**
* Gives what kind of Result this implements. This is to help a json processor client
* to know what kind of Result it is from the JSON packet of {@link Run}
*
* We don't serialize it in json as Jackson already adds resultType object.
*
* @return One of {@link Type}
*/
@JsonIgnore
Type getType();
public Result(@Nonnull @JsonProperty("type") String type, @Nonnull @JsonProperty("data") Map<String, ?> data) {
this.type = type;
this.data = ImmutableMap.copyOf(data);
}
}

View File

@ -65,9 +65,6 @@ public final class Run {
@JsonProperty("result")
public final Result result;
@JsonProperty("resultType")
public final Result.Type resultType;
public enum Status {
/** Build completed successfully */
SUCCESSFUL,
@ -129,11 +126,11 @@ public final class Run {
this.runTrend = runTrend;
this.enQueueTime = new Date(enQueueTime.getTime());
this.result = result;
if(result != null) {
this.resultType = result.getType();
}else{
this.resultType = null;
}
// if(result != null) {
// this.resultType = result.getType();
// }else{
// this.resultType = null;
// }
}

View File

@ -1,7 +1,8 @@
package io.jenkins.blueocean.api.pipeline;
import com.google.common.collect.ImmutableList;
import io.jenkins.blueocean.api.pipeline.model.JobResult;
import com.google.common.collect.ImmutableMap;
import io.jenkins.blueocean.api.pipeline.model.Result;
import io.jenkins.blueocean.api.pipeline.model.Run;
import io.jenkins.blueocean.commons.JsonConverter;
import org.junit.Assert;
@ -26,7 +27,7 @@ public class FindPipelineRunsResponseTest {
.durationInMillis(10000L)
.runSummary("build sucessful")
.runTrend(Run.RunTrend.FIXED)
.result(new JobResult())
.result(new Result("job", ImmutableMap.of("status", "success")))
.build(),
new Run.Builder("24","test1","cloudbees")
.branch("master")
@ -38,7 +39,7 @@ public class FindPipelineRunsResponseTest {
.durationInMillis(10000L)
.runSummary("build sucessful")
.runTrend(Run.RunTrend.FIXED)
.result(new JobResult())
.result(new Result("job",ImmutableMap.of("status", "success")))
.build()), null, null);
String json = JsonConverter.toJson(response);
@ -64,7 +65,7 @@ public class FindPipelineRunsResponseTest {
Assert.assertEquals(expected.enQueueTime, actual.enQueueTime);
Assert.assertEquals(expected.runSummary, actual.runSummary);
Assert.assertEquals(expected.runTrend, actual.runTrend);
Assert.assertTrue(actual.result instanceof JobResult);
// Assert.assertTrue(actual.result instanceof JobResult);
}
System.out.println("Converted back from Json:\n"+JsonConverter.toJson(responseFromJson));

View File

@ -1,6 +1,7 @@
package io.jenkins.blueocean.api.pipeline;
import io.jenkins.blueocean.api.pipeline.model.JobResult;
import com.google.common.collect.ImmutableMap;
import io.jenkins.blueocean.api.pipeline.model.Result;
import io.jenkins.blueocean.api.pipeline.model.Run;
import io.jenkins.blueocean.commons.JsonConverter;
import org.junit.Assert;
@ -27,7 +28,7 @@ public class GetPipelineRunResponseTest {
.durationInMillis(10000L)
.runSummary("build sucessful")
.runTrend(Run.RunTrend.FIXED)
.result(new JobResult())
.result(new Result("job",ImmutableMap.of("status", "success")))
.build());
String json = JsonConverter.toJson(response);
@ -50,7 +51,7 @@ public class GetPipelineRunResponseTest {
Assert.assertEquals(expected.enQueueTime, actual.enQueueTime);
Assert.assertEquals(expected.runSummary, actual.runSummary);
Assert.assertEquals(expected.runTrend, actual.runTrend);
Assert.assertTrue(actual.result instanceof JobResult);
Assert.assertEquals(expected.result.type, actual.result.type);
System.out.println("Converted back from Json:\n"+JsonConverter.toJson(responseFromJson));
}

View File

@ -10,7 +10,7 @@ import org.junit.Test;
public class GetPipelineRunTest {
@Test
public void serializeDeserialize(){
GetPipelineRunRequest request = new GetPipelineRunRequest("cloudbees", "test1");
GetPipelineRunRequest request = new GetPipelineRunRequest("cloudbees", "test1", null);
String json = JsonConverter.toJson(request);

View File

@ -16,8 +16,8 @@
<name>Blue Ocean UI Parent</name>
<properties>
<java.level>6</java.level>
<jackson.version>2.2.3</jackson.version>
<java.level>7</java.level>
<jackson.version>2.2.3</jackson.version>
<jenkins.version>1.639</jenkins.version>
</properties>
@ -29,6 +29,7 @@
<module>pipeline-service-api</module>
<module>embedded-driver</module>
<module>blueocean-github-oauth-plugin</module>
<module>blueocean-rest</module>
<module>alpha</module>
<module>bravo</module>
<module>war</module>

View File

@ -12,10 +12,10 @@ import javax.annotation.Nullable;
* @author Vivek Pandey
*/
public final class GetUserDetailsResponse{
@JsonProperty("userDetails")
@JsonProperty("user")
public final UserDetails userDetails;
public GetUserDetailsResponse(@Nullable @JsonProperty("userDetails")UserDetails userDetails) {
public GetUserDetailsResponse(@Nullable @JsonProperty("user")UserDetails userDetails) {
this.userDetails = userDetails;
}
}

View File

@ -69,6 +69,26 @@
<type>hpi</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.jenkins.blueocean</groupId>
<artifactId>blueocean-rest</artifactId>
<version>${project.version}</version>
<type>hpi</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.jenkins.blueocean</groupId>
<artifactId>embedded-driver</artifactId>
<version>${project.version}</version>
<type>hpi</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.4</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>