Feature/jenkins 38848: Credential enumeration API (#593)

* Adding routable API to blueocean api path

* JENKINS-38848# Credential GET API

- Allows plugins to serve their object graphs from /organizations/:id/ API.
- Doesn't quite work for credentials plugin as for POST requests it requires form submissions

* Credential search API

- credential reponse description elements defaults to displayName:domain:type.

* Fixed links

* Organization route extensibility simplified

- OrganizationAction is all one needs to expose it's object graph inside organization route
- ApiRoute remains to be extension point to be added at root of bluocean route

* Added missing file

* Added domain to credential object.

* Credential creation API

* Refactoring. Moved OrganizationRoute to blueocean-rest.
This commit is contained in:
Vivek Pandey 2016-12-29 23:49:16 +05:30 committed by GitHub
parent 873c0c2f85
commit 71b813e2c6
13 changed files with 607 additions and 25 deletions

View File

@ -75,6 +75,12 @@
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>credentials</artifactId>
<version>2.1.6</version>
</dependency>
<!-- Not needed by blueocean runtime but adds to blueocean experience -->
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>

View File

@ -0,0 +1,213 @@
package io.jenkins.blueocean.rest.impl.pipeline.credential;
import com.cloudbees.plugins.credentials.CredentialsStoreAction;
import com.cloudbees.plugins.credentials.common.IdCredentials;
import com.cloudbees.plugins.credentials.domains.Domain;
import io.jenkins.blueocean.commons.ServiceException;
import io.jenkins.blueocean.rest.Navigable;
import io.jenkins.blueocean.rest.Reachable;
import io.jenkins.blueocean.rest.hal.Link;
import io.jenkins.blueocean.rest.model.Container;
import io.jenkins.blueocean.rest.model.CreateResponse;
import io.jenkins.blueocean.rest.model.Resource;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.WebMethod;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.json.JsonBody;
import org.kohsuke.stapler.verb.POST;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
/**
* Credential API implementation.
*
* TODO: Remove it once proper REST API is implemented in Credentials plugin
*
* @author Vivek Pandey
*/
public class CredentialApi extends Resource {
private final CredentialsStoreAction credentialStoreAction;
private final Reachable parent;
public CredentialApi(CredentialsStoreAction ca, Reachable parent) {
this.credentialStoreAction = ca;
this.parent = parent;
}
@Exported
public String getStore(){
return credentialStoreAction.getUrlName();
}
@Navigable
public Container<CredentialDomain> getDomains(){
return new Container<CredentialDomain>() {
private final Link self = CredentialApi.this.getLink().rel("domains");
Map<String, CredentialsStoreAction.DomainWrapper> map = credentialStoreAction.getDomains();
@Override
public CredentialDomain get(String name) {
return new CredentialDomain(map.get(name), getLink());
}
@Override
public Link getLink() {
return self;
}
@Override
public Iterator<CredentialDomain> iterator() {
final Iterator<CredentialsStoreAction.DomainWrapper> i = map.values().iterator();
return new Iterator<CredentialDomain>(){
@Override
public boolean hasNext() {
return i.hasNext();
}
@Override
public CredentialDomain next() {
return new CredentialDomain(i.next(), getLink());
}
@Override
public void remove() {
throw new ServiceException.NotImplementedException("Not implemented yet");
}
};
}
};
}
@Override
public Link getLink() {
return parent.getLink().rel(getStore());
}
public static class CredentialDomain extends Resource{
private final Link self;
private final CredentialsStoreAction.DomainWrapper domainWrapper;
public CredentialDomain(CredentialsStoreAction.DomainWrapper domainWrapper, Link parent) {
this.self = parent.rel(domainWrapper.getUrlName());
this.domainWrapper = domainWrapper;
}
@Exported(inline = true, merge = true)
public CredentialsStoreAction.DomainWrapper getDomain(){
return domainWrapper;
}
@Override
public Link getLink() {
return self;
}
@Navigable
public CredentialValueContainer getCredentials(){
return new CredentialValueContainer(domainWrapper, this);
}
}
public static class CredentialValueContainer extends Container<Credential>{
private final CredentialsStoreAction.DomainWrapper domainWrapper;
private final Link self;
public CredentialValueContainer(CredentialsStoreAction.DomainWrapper domainWrapper, Reachable parent) {
this.domainWrapper = domainWrapper;
this.self = parent.getLink().rel("credentials");
}
@Override
public Credential get(String name) {
return new Credential(domainWrapper.getCredential(name), self);
}
@POST
@WebMethod(name = "")
public CreateResponse create(@JsonBody JSONObject body, StaplerRequest request) throws IOException {
final IdCredentials credentials = request.bindJSON(IdCredentials.class, body.getJSONObject("credentials"));
domainWrapper.getStore().addCredentials(domainWrapper.getDomain(), credentials);
final Domain domain = domainWrapper.getDomain();
domainWrapper.getStore().addCredentials(domain, credentials);
return new CreateResponse(new Credential(domainWrapper.getCredentials().get(credentials.getId()), getLink()));
}
@Override
public Link getLink() {
return self;
}
@Override
public Iterator<Credential> iterator() {
final Iterator<CredentialsStoreAction.CredentialsWrapper> i = domainWrapper.getCredentialsList().iterator();
return new Iterator<Credential>(){
@Override
public boolean hasNext() {
return i.hasNext();
}
@Override
public Credential next() {
return new Credential(i.next(),self);
}
@Override
public void remove() {
throw new ServiceException.NotImplementedException("Not implemented yet");
}
};
}
}
public static class Credential extends Resource{
private final Link self;
private final CredentialsStoreAction.CredentialsWrapper credentialsWrapper;
public Credential(CredentialsStoreAction.CredentialsWrapper credentialsWrapper, Link parent) {
this.self = parent.rel(credentialsWrapper.getUrlName());
this.credentialsWrapper = credentialsWrapper;
}
@Exported(merge = true, inline = true)
public CredentialsStoreAction.CredentialsWrapper getCredential(){
return credentialsWrapper;
}
@Override
public Link getLink() {
return self;
}
@Exported
public String getDomain(){
return credentialsWrapper.getDomain().getUrlName();
}
/**
* If description is empty its displayName:domain:type, otherwise just the given description name
*/
@Exported
public String getDescription(){
if(credentialsWrapper.getDescription() == null || credentialsWrapper.getDescription().trim().isEmpty()){
return String.format("%s:%s:%s",credentialsWrapper.getDisplayName(),credentialsWrapper.getDomain().getUrlName(), credentialsWrapper.getTypeName());
}
return credentialsWrapper.getDescription();
}
}
}

View File

@ -0,0 +1,70 @@
package io.jenkins.blueocean.rest.impl.pipeline.credential;
import com.cloudbees.plugins.credentials.CredentialsStoreAction;
import com.cloudbees.plugins.credentials.ViewCredentialsAction;
import hudson.Extension;
import hudson.ExtensionList;
import io.jenkins.blueocean.rest.hal.Link;
import io.jenkins.blueocean.rest.model.BlueOrganization;
import io.jenkins.blueocean.rest.model.Container;
import io.jenkins.blueocean.rest.OrganizationRoute;
import org.kohsuke.stapler.export.ExportedBean;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Credential API container
*
* TODO: can very well be moved in it's own plugin along with {@link CredentialApi}
*
* @author Vivek Pandey
*/
@Extension
@ExportedBean
public class CredentialContainer extends Container<CredentialApi> implements OrganizationRoute {
private final Link self;
public CredentialContainer() {
BlueOrganization organization=null;
for(BlueOrganization action: ExtensionList.lookup(BlueOrganization.class)){
organization = action;
};
this.self = (organization != null) ? organization.getLink().rel("credentials")
: new Link("/organizations/jenkins/credentials/");
}
@Override
public String getUrlName() {
return "credentials";
}
@Override
public Link getLink() {
return self;
}
@Override
public CredentialApi get(String name) {
for(CredentialApi api: this){
if(api.getStore().equals(name)){
return api;
}
}
return null;
}
@Override
public Iterator<CredentialApi> iterator() {
List<CredentialApi> apis = new ArrayList<>();
for(ViewCredentialsAction action: ExtensionList.lookup(ViewCredentialsAction.class)){
for(CredentialsStoreAction c:action.getStoreActions()){
apis.add(new CredentialApi(c, this));
}
};
return apis.iterator();
}
}

View File

@ -0,0 +1,59 @@
package io.jenkins.blueocean.rest.impl.pipeline.credential;
import hudson.Extension;
import hudson.ExtensionList;
import io.jenkins.blueocean.commons.ServiceException;
import io.jenkins.blueocean.rest.OmniSearch;
import io.jenkins.blueocean.rest.Query;
import io.jenkins.blueocean.rest.pageable.Pageable;
import io.jenkins.blueocean.rest.pageable.Pageables;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Credential search API
* @author Vivek Pandey
*/
@Extension
public class CredentialSearch extends OmniSearch<CredentialApi.Credential> {
@Override
public String getType() {
return "credential";
}
@Override
public Pageable<CredentialApi.Credential> search(Query q) {
List<CredentialApi.Credential> credentials = new ArrayList<>();
String domain = q.param("domain");
ExtensionList<CredentialContainer> extensionList = ExtensionList.lookup(CredentialContainer.class);
if(!extensionList.isEmpty()) {
CredentialContainer credentialContainer = extensionList.get(0);
Iterator<CredentialApi> it = credentialContainer.iterator();
if (it.hasNext()) {
CredentialApi api = it.next();
if(domain != null){
CredentialApi.CredentialDomain d = api.getDomains().get(domain);
if(d == null){
throw new ServiceException.BadRequestExpception("Credential domain "+ domain +" not found");
}
for (CredentialApi.Credential c : d.getCredentials()) {
credentials.add(c);
}
}else {
for (CredentialApi.CredentialDomain d : api.getDomains()) {
for (CredentialApi.Credential c : d.getCredentials()) {
credentials.add(c);
}
}
}
}
}
return Pageables.wrap(credentials);
}
}

View File

@ -0,0 +1,165 @@
package io.jenkins.blueocean.rest.impl.pipeline;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.CredentialsStore;
import com.cloudbees.plugins.credentials.CredentialsStoreAction;
import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
import com.cloudbees.plugins.credentials.ViewCredentialsAction;
import com.cloudbees.plugins.credentials.domains.Domain;
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
import com.google.common.collect.ImmutableMap;
import hudson.ExtensionList;
import org.junit.Assert;
import org.junit.Test;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* @author Vivek Pandey
*/
public class CredentialApiTest extends PipelineBaseTest {
@Test
public void listCredentials() throws IOException {
SystemCredentialsProvider.ProviderImpl system = ExtensionList.lookup(CredentialsProvider.class).get(SystemCredentialsProvider.ProviderImpl.class);
CredentialsStore systemStore = system.getStore(j.getInstance());
systemStore.addDomain(new Domain("domain1", null, null));
systemStore.addCredentials(systemStore.getDomainByName("domain1"), new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, null,null, "admin", "pass$wd"));
CredentialsStoreAction credentialsStoreAction = ExtensionList.lookup(ViewCredentialsAction.class).get(0).getStore("system");
CredentialsStoreAction.DomainWrapper domainWrapper = credentialsStoreAction.getDomain("domain1");
CredentialsStoreAction.CredentialsWrapper credentialsWrapper = domainWrapper.getCredentialsList().get(0);
List<Map> creds = get("/organizations/jenkins/credentials/system/domains/domain1/credentials/", List.class);
Assert.assertEquals(1, creds.size());
Map cred = creds.get(0);
Assert.assertNotNull(cred.get("id"));
Map cred1 = get("/organizations/jenkins/credentials/system/domains/domain1/credentials/"+cred.get("id")+"/");
Assert.assertEquals(credentialsWrapper.getId(),cred1.get("id"));
Assert.assertEquals(credentialsWrapper.getTypeName(),cred1.get("typeName"));
Assert.assertEquals(credentialsWrapper.getDisplayName(),cred1.get("displayName"));
Assert.assertEquals(credentialsWrapper.getFullName(),cred1.get("fullName"));
Assert.assertEquals(String.format("%s:%s:%s", credentialsWrapper.getDisplayName(), credentialsWrapper.getDomain().getUrlName(), credentialsWrapper.getTypeName()),cred1.get("description"));
Assert.assertEquals(credentialsWrapper.getDomain().getUrlName(),cred1.get("domain"));
}
@Test
public void listAllCredentials() throws IOException {
SystemCredentialsProvider.ProviderImpl system = ExtensionList.lookup(CredentialsProvider.class).get(SystemCredentialsProvider.ProviderImpl.class);
CredentialsStore systemStore = system.getStore(j.getInstance());
systemStore.addDomain(new Domain("domain1", null, null));
systemStore.addDomain(new Domain("domain2", null, null));
systemStore.addCredentials(systemStore.getDomainByName("domain1"), new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, null,null, "admin", "pass$wd"));
systemStore.addCredentials(systemStore.getDomainByName("domain2"), new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, null,null, "joe", "pass$wd"));
CredentialsStoreAction credentialsStoreAction = ExtensionList.lookup(ViewCredentialsAction.class).get(0).getStore("system");
CredentialsStoreAction.DomainWrapper domain1 = credentialsStoreAction.getDomain("domain1");
CredentialsStoreAction.DomainWrapper domain2 = credentialsStoreAction.getDomain("domain2");
CredentialsStoreAction.CredentialsWrapper credentials1 = domain1.getCredentialsList().get(0);
CredentialsStoreAction.CredentialsWrapper credentials2 = domain2.getCredentialsList().get(0);
List<Map> creds = get("/search?q=type:credential", List.class);
Assert.assertEquals(2, creds.size());
Assert.assertEquals(credentials1.getId(), creds.get(0).get("id"));
Assert.assertEquals(credentials2.getId(), creds.get(1).get("id"));
creds = get("/search?q=type:credential;domain:domain2", List.class);
Assert.assertEquals(1, creds.size());
Assert.assertEquals(credentials2.getId(), creds.get(0).get("id"));
}
@Test
public void createSshCredentialUsingSshFileOnMaster() throws IOException {
SystemCredentialsProvider.ProviderImpl system = ExtensionList.lookup(CredentialsProvider.class).get(SystemCredentialsProvider.ProviderImpl.class);
CredentialsStore systemStore = system.getStore(j.getInstance());
systemStore.addDomain(new Domain("domain1", null, null));
Map<String, Object> resp = post("/organizations/jenkins/credentials/system/domains/domain1/credentials/",
ImmutableMap.of("credentials",
new ImmutableMap.Builder<String,Object>()
.put("privateKeySource", ImmutableMap.of("privateKeyFile", "~/.ssh/blah", "stapler-class", "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey$FileOnMasterPrivateKeySource"))
.put("passphrase", "ssh2")
.put("scope", "GLOBAL")
.put("description", "ssh2 desc")
.put("$class", "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey")
.put("username", "ssh2").build()
)
, 201);
Assert.assertEquals("SSH Username with private key", resp.get("typeName"));
Assert.assertEquals("domain1", resp.get("domain"));
}
@Test
public void createSshCredentialUsingDefaultSshOnMaster() throws IOException {
SystemCredentialsProvider.ProviderImpl system = ExtensionList.lookup(CredentialsProvider.class).get(SystemCredentialsProvider.ProviderImpl.class);
CredentialsStore systemStore = system.getStore(j.getInstance());
systemStore.addDomain(new Domain("domain1", null, null));
Map<String, Object> resp = post("/organizations/jenkins/credentials/system/domains/domain1/credentials/",
ImmutableMap.of("credentials",
new ImmutableMap.Builder<String,Object>()
.put("privateKeySource", ImmutableMap.of("stapler-class", "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey$UsersPrivateKeySource"))
.put("passphrase", "ssh2")
.put("scope", "GLOBAL")
.put("description", "ssh2 desc")
.put("$class", "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey")
.put("username", "ssh2").build()
)
, 201);
Assert.assertEquals("SSH Username with private key", resp.get("typeName"));
Assert.assertEquals("domain1", resp.get("domain"));
}
@Test
public void createSshCredentialUsingDirectSsh() throws IOException {
SystemCredentialsProvider.ProviderImpl system = ExtensionList.lookup(CredentialsProvider.class).get(SystemCredentialsProvider.ProviderImpl.class);
CredentialsStore systemStore = system.getStore(j.getInstance());
systemStore.addDomain(new Domain("domain1", null, null));
Map<String, Object> resp = post("/organizations/jenkins/credentials/system/domains/domain1/credentials/",
ImmutableMap.of("credentials",
new ImmutableMap.Builder<String,Object>()
.put("privateKeySource", ImmutableMap.of(
"privateKey", "abcabc1212",
"stapler-class", "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey$DirectEntryPrivateKeySource"))
.put("passphrase", "ssh2")
.put("scope", "GLOBAL")
.put("description", "ssh2 desc")
.put("$class", "com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey")
.put("username", "ssh2").build()
)
, 201);
Assert.assertEquals("SSH Username with private key", resp.get("typeName"));
Assert.assertEquals("domain1", resp.get("domain"));
}
@Test
public void createUsingUsernamePassword() throws IOException {
SystemCredentialsProvider.ProviderImpl system = ExtensionList.lookup(CredentialsProvider.class).get(SystemCredentialsProvider.ProviderImpl.class);
CredentialsStore systemStore = system.getStore(j.getInstance());
systemStore.addDomain(new Domain("domain1", null, null));
Map<String, Object> resp = post("/organizations/jenkins/credentials/system/domains/domain1/credentials/",
ImmutableMap.of("credentials",
new ImmutableMap.Builder<String,Object>()
.put("password", "abcd")
.put("stapler-class", "com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl")
.put("scope", "GLOBAL")
.put("description", "joe desc")
.put("$class", "com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl")
.put("username", "joe").build()
)
, 201);
Assert.assertEquals("Username with password", resp.get("typeName"));
Assert.assertEquals("domain1", resp.get("domain"));
}
}

View File

@ -1,19 +1,21 @@
package io.jenkins.blueocean.service.embedded.rest;
import com.google.common.base.Objects;
import hudson.Util;
import hudson.ExtensionList;
import hudson.model.Action;
import hudson.model.User;
import io.jenkins.blueocean.commons.ServiceException;
import io.jenkins.blueocean.commons.stapler.JsonBody;
import io.jenkins.blueocean.rest.ApiHead;
import io.jenkins.blueocean.rest.OrganizationRoute;
import io.jenkins.blueocean.rest.hal.Link;
import io.jenkins.blueocean.rest.model.BlueOrganization;
import io.jenkins.blueocean.rest.model.BluePipelineContainer;
import io.jenkins.blueocean.rest.model.BlueUser;
import io.jenkins.blueocean.rest.model.BlueUserContainer;
import io.jenkins.blueocean.rest.model.GenericResource;
import jenkins.model.Jenkins;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.WebMethod;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.verb.DELETE;
import org.kohsuke.stapler.verb.PUT;
@ -35,9 +37,6 @@ public class OrganizationImpl extends BlueOrganization {
*/
public static final OrganizationImpl INSTANCE = new OrganizationImpl();
private OrganizationImpl() {
}
/**
* In embedded mode, there's only one organization
*/
@ -95,4 +94,39 @@ public class OrganizationImpl extends BlueOrganization {
return ApiHead.INSTANCE().getLink().rel("organizations/"+getName());
}
/**
* Give plugins chance to handle this API route.
*
* @param route URL path that needs handling. e.g. for requested url /rest/organizations/:id/xyz, route param value will be 'xyz'
* @return stapler object that can handle give route. Could be null
*/
public Object getDynamic(String route){
//First look for OrganizationActions
for(OrganizationRoute organizationRoute: ExtensionList.lookup(OrganizationRoute.class)){
if(organizationRoute.getUrlName() != null && organizationRoute.getUrlName().equals(route)){
return wrap(organizationRoute);
}
}
// No OrganizationRoute found, now lookup in available actions from Jenkins instance serving root
for(Action action:Jenkins.getInstance().getActions()) {
if (action.getUrlName() != null && action.getUrlName().equals(route)) {
return wrap(action);
}
}
return null;
}
private Object wrap(Object action){
if (isExportedBean(action.getClass())) {
return action;
} else {
return new GenericResource<>(action);
}
}
private boolean isExportedBean(Class clz){
return clz.getAnnotation(ExportedBean.class) != null;
}
}

View File

@ -31,6 +31,8 @@ public final class ApiHead implements RootRoutable, Reachable {
private volatile Map<String,ApiRoutable> apis;
public static final String URL_NAME="rest";
/**
* Search API
*
@ -55,7 +57,7 @@ public final class ApiHead implements RootRoutable, Reachable {
*/
@Override
public String getUrlName() {
return "rest";
return URL_NAME;
}
/**
@ -119,7 +121,7 @@ public final class ApiHead implements RootRoutable, Reachable {
if(apiMap == null){
Map<String,ApiRoutable> apiMapTmp = new HashMap<>();
for ( ApiRoutable api : ExtensionList.lookup(ApiRoutable.class)) {
apiMapTmp.put(api.getUrlName(),api);
apiMapTmp.put(api.getUrlName(), api);
}
apis = apiMap = apiMapTmp;
}

View File

@ -8,6 +8,7 @@ import io.jenkins.blueocean.Routable;
* Marks the REST API endpoints that are exposed by {@link ApiHead}
*
* @author Kohsuke Kawaguchi
* @author Vivek Pandey
*/
public interface ApiRoutable extends Routable, ExtensionPoint {
/**

View File

@ -0,0 +1,12 @@
package io.jenkins.blueocean.rest;
import hudson.ExtensionPoint;
import io.jenkins.blueocean.Routable;
/**
* Route contributing to {@link io.jenkins.blueocean.rest.model.BlueOrganization}: url path /organization/:id/:organizationRoute.urlName()
*
* @author Vivek Pandey
*/
public interface OrganizationRoute extends Routable, ExtensionPoint {
}

View File

@ -66,7 +66,6 @@ public abstract class BlueExtensionClassContainer implements ApiRoutable, Extens
public String getUrlName() {
return "classes";
}
}

View File

@ -1,5 +1,6 @@
package io.jenkins.blueocean.rest.model;
import io.jenkins.blueocean.Routable;
import io.jenkins.blueocean.rest.Navigable;
import io.jenkins.blueocean.rest.annotation.Capability;
import org.kohsuke.stapler.export.Exported;
@ -12,11 +13,16 @@ import static io.jenkins.blueocean.rest.model.KnownCapabilities.BLUE_ORGANIZATIO
* @author Kohsuke Kawaguchi
*/
@Capability(BLUE_ORGANIZATION)
public abstract class BlueOrganization extends Resource {
public abstract class BlueOrganization extends Resource implements Routable{
public static final String NAME="name";
public static final String DISPLAY_NAME="name";
public static final String PIPELINES="pipelines";
@Override
public String getUrlName() {
return getName();
}
@Exported(name = NAME)
public abstract String getName();
@ -42,5 +48,6 @@ public abstract class BlueOrganization extends Resource {
*/
@Navigable
public abstract BlueUser getUser();
}

View File

@ -4,9 +4,10 @@ import hudson.ExtensionPoint;
import io.jenkins.blueocean.rest.ApiRoutable;
/**
* This is the head of the blue ocean API.
* Container of BlueOcean {@link BlueOrganization}s
*
* @author Kohsuke Kawaguchi
* @author Vivek Pandey
*/
public abstract class BlueOrganizationContainer extends Container<BlueOrganization> implements ApiRoutable, ExtensionPoint {

View File

@ -57,21 +57,15 @@ public class GenericResource<T> extends Resource {
// TODO: this only take care of getXyz() style methods. It needs to take care of other types of url
// path handling
Method m = clz.getMethod("get"+ Character.toUpperCase(token.charAt(0))+token.substring(1));
Link subResLink = getLink().rel(token+"/");
final Object v = m.invoke(value);
if(v instanceof List){
return Containers.from(subResLink,(List)v);
}else if(v instanceof Map){
return Containers.from(subResLink,(Map)v);
}else if(v instanceof String){
return new PrimitiveTypeResource(subResLink,v);
return getResource(token, m);
} catch (NoSuchMethodException e) {
for(Method m:clz.getMethods()){
Exported exported = m.getAnnotation(Exported.class);
if(exported != null && exported.name().equals(token)){
return getResource(token, m);
}
}
return new GenericResource<>(v);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
LOGGER.error(e.getMessage(), e);
throw new ServiceException.NotFoundException("Path "+token+" is not found");
return null;
}
}
@ -80,6 +74,25 @@ public class GenericResource<T> extends Resource {
return (self !=null) ? self.getLink() : new Link(Stapler.getCurrentRequest().getPathInfo());
}
private Object getResource(String token, Method m){
final Object v;
try {
v = m.invoke(value);
} catch (IllegalAccessException | InvocationTargetException e) {
LOGGER.error(e.getMessage(), e);
throw new ServiceException.NotFoundException("Path "+token+" is not found");
}
Link subResLink = getLink().rel(token+"/");
if(v instanceof List){
return Containers.from(subResLink,(List)v);
}else if(v instanceof Map){
return Containers.from(subResLink,(Map)v);
}else if(v instanceof String){
return new PrimitiveTypeResource(subResLink,v);
}
return new GenericResource<>(v);
}
/**
* Resource that exposes primitive type value as JSON bean