Thomas Vachuska
Committed by Gerrit Code Review

ONOS-1684 Added support for app dependencies.

Change-Id: Iae318c24c3c9bd43d84318c79ac420fc85d5d599
......@@ -41,7 +41,7 @@ public class ApplicationsListCommand extends AbstractShellCommand {
private static final String FMT =
"%s id=%d, name=%s, version=%s, origin=%s, description=%s, " +
"features=%s, featuresRepo=%s, permissions=%s";
"features=%s, featuresRepo=%s, apps=%s, permissions=%s";
private static final String SHORT_FMT =
"%s %3d %-32s %-8s %s";
......@@ -76,7 +76,7 @@ public class ApplicationsListCommand extends AbstractShellCommand {
app.id().id(), app.id().name(), app.version(), app.origin(),
app.description(), app.features(),
app.featuresRepo().isPresent() ? app.featuresRepo().get().toString() : "",
app.permissions());
app.requiredApps(), app.permissions());
}
}
}
......
......@@ -86,4 +86,11 @@ public interface ApplicationDescription {
* @return application features
*/
List<String> features();
/**
* Returns list of required application names.
*
* @return list of application names
*/
List<String> requiredApps();
}
......
......@@ -76,7 +76,7 @@ public interface ApplicationStore extends Store<ApplicationEvent, ApplicationSto
void remove(ApplicationId appId);
/**
* Mark the application as actived.
* Mark the application as active.
*
* @param appId application identifier
*/
......
......@@ -41,6 +41,7 @@ public class DefaultApplicationDescription implements ApplicationDescription {
private final Set<Permission> permissions;
private final Optional<URI> featuresRepo;
private final List<String> features;
private final List<String> requiredApps;
/**
* Creates a new application descriptor using the supplied data.
......@@ -53,11 +54,13 @@ public class DefaultApplicationDescription implements ApplicationDescription {
* @param permissions requested permissions
* @param featuresRepo optional features repo URI
* @param features application features
* @param requiredApps list of required application names
*/
public DefaultApplicationDescription(String name, Version version,
String description, String origin,
ApplicationRole role, Set<Permission> permissions,
URI featuresRepo, List<String> features) {
URI featuresRepo, List<String> features,
List<String> requiredApps) {
this.name = checkNotNull(name, "Name cannot be null");
this.version = checkNotNull(version, "Version cannot be null");
this.description = checkNotNull(description, "Description cannot be null");
......@@ -66,6 +69,7 @@ public class DefaultApplicationDescription implements ApplicationDescription {
this.permissions = checkNotNull(permissions, "Permissions cannot be null");
this.featuresRepo = Optional.ofNullable(featuresRepo);
this.features = checkNotNull(features, "Features cannot be null");
this.requiredApps = checkNotNull(requiredApps, "Required apps cannot be null");
checkArgument(!features.isEmpty(), "There must be at least one feature");
}
......@@ -110,6 +114,11 @@ public class DefaultApplicationDescription implements ApplicationDescription {
}
@Override
public List<String> requiredApps() {
return requiredApps;
}
@Override
public String toString() {
return toStringHelper(this)
.add("name", name)
......@@ -120,6 +129,7 @@ public class DefaultApplicationDescription implements ApplicationDescription {
.add("permissions", permissions)
.add("featuresRepo", featuresRepo)
.add("features", features)
.add("requiredApps", requiredApps)
.toString();
}
}
......
......@@ -84,4 +84,11 @@ public interface Application {
* @return application features
*/
List<String> features();
/**
* Returns list of required application names.
*
* @return list of application names
*/
List<String> requiredApps();
}
......
......@@ -40,6 +40,7 @@ public class DefaultApplication implements Application {
private final Set<Permission> permissions;
private final Optional<URI> featuresRepo;
private final List<String> features;
private final List<String> requiredApps;
/**
* Creates a new application descriptor using the supplied data.
......@@ -52,11 +53,13 @@ public class DefaultApplication implements Application {
* @param permissions requested permissions
* @param featuresRepo optional features repo URI
* @param features application features
* @param requiredApps list of required application names
*/
public DefaultApplication(ApplicationId appId, Version version,
String description, String origin,
ApplicationRole role, Set<Permission> permissions,
Optional<URI> featuresRepo, List<String> features) {
Optional<URI> featuresRepo, List<String> features,
List<String> requiredApps) {
this.appId = checkNotNull(appId, "ID cannot be null");
this.version = checkNotNull(version, "Version cannot be null");
this.description = checkNotNull(description, "Description cannot be null");
......@@ -65,6 +68,7 @@ public class DefaultApplication implements Application {
this.permissions = checkNotNull(permissions, "Permissions cannot be null");
this.featuresRepo = checkNotNull(featuresRepo, "Features repo cannot be null");
this.features = checkNotNull(features, "Features cannot be null");
this.requiredApps = checkNotNull(requiredApps, "Required apps cannot be null");
checkArgument(!features.isEmpty(), "There must be at least one feature");
}
......@@ -109,9 +113,14 @@ public class DefaultApplication implements Application {
}
@Override
public List<String> requiredApps() {
return requiredApps;
}
@Override
public int hashCode() {
return Objects.hash(appId, version, description, origin, role, permissions,
featuresRepo, features);
featuresRepo, features, requiredApps);
}
@Override
......@@ -130,7 +139,8 @@ public class DefaultApplication implements Application {
Objects.equals(this.role, other.role) &&
Objects.equals(this.permissions, other.permissions) &&
Objects.equals(this.featuresRepo, other.featuresRepo) &&
Objects.equals(this.features, other.features);
Objects.equals(this.features, other.features) &&
Objects.equals(this.requiredApps, other.requiredApps);
}
@Override
......@@ -144,6 +154,7 @@ public class DefaultApplication implements Application {
.add("permissions", permissions)
.add("featuresRepo", featuresRepo)
.add("features", features)
.add("requiredApps", requiredApps)
.toString();
}
}
......
......@@ -33,7 +33,7 @@ public class ApplicationEventTest extends AbstractEventTest {
private Application createApp() {
return new DefaultApplication(APP_ID, VER, DESC, ORIGIN, ROLE,
PERMS, Optional.of(FURL), FEATURES);
PERMS, Optional.of(FURL), FEATURES, APPS);
}
@Test
......
......@@ -46,12 +46,13 @@ public class DefaultApplicationDescriptionTest {
new Permission(AppPermission.class.getName(), "FLOWRULE_READ"));
public static final URI FURL = URI.create("mvn:org.foo-features/1.2a/xml/features");
public static final List<String> FEATURES = ImmutableList.of("foo", "bar");
public static final List<String> APPS = ImmutableList.of("fifi");
@Test
public void basics() {
ApplicationDescription app =
new DefaultApplicationDescription(APP_NAME, VER, DESC, ORIGIN,
ROLE, PERMS, FURL, FEATURES);
ROLE, PERMS, FURL, FEATURES, APPS);
assertEquals("incorrect id", APP_NAME, app.name());
assertEquals("incorrect version", VER, app.version());
assertEquals("incorrect description", DESC, app.description());
......@@ -60,6 +61,7 @@ public class DefaultApplicationDescriptionTest {
assertEquals("incorrect permissions", PERMS, app.permissions());
assertEquals("incorrect features repo", FURL, app.featuresRepo().get());
assertEquals("incorrect features", FEATURES, app.features());
assertEquals("incorrect apps", APPS, app.requiredApps());
assertTrue("incorrect toString", app.toString().contains(APP_NAME));
}
......
......@@ -34,7 +34,7 @@ public class DefaultApplicationTest {
@Test
public void basics() {
Application app = new DefaultApplication(APP_ID, VER, DESC, ORIGIN, ROLE,
PERMS, Optional.of(FURL), FEATURES);
PERMS, Optional.of(FURL), FEATURES, APPS);
assertEquals("incorrect id", APP_ID, app.id());
assertEquals("incorrect version", VER, app.version());
assertEquals("incorrect description", DESC, app.description());
......@@ -43,19 +43,20 @@ public class DefaultApplicationTest {
assertEquals("incorrect permissions", PERMS, app.permissions());
assertEquals("incorrect features repo", FURL, app.featuresRepo().get());
assertEquals("incorrect features", FEATURES, app.features());
assertEquals("incorrect apps", APPS, app.requiredApps());
assertTrue("incorrect toString", app.toString().contains(APP_NAME));
}
@Test
public void testEquality() {
Application a1 = new DefaultApplication(APP_ID, VER, DESC, ORIGIN, ROLE,
PERMS, Optional.of(FURL), FEATURES);
PERMS, Optional.of(FURL), FEATURES, APPS);
Application a2 = new DefaultApplication(APP_ID, VER, DESC, ORIGIN, ROLE,
PERMS, Optional.of(FURL), FEATURES);
PERMS, Optional.of(FURL), FEATURES, APPS);
Application a3 = new DefaultApplication(APP_ID, VER, DESC, ORIGIN, ROLE,
PERMS, Optional.empty(), FEATURES);
PERMS, Optional.empty(), FEATURES, APPS);
Application a4 = new DefaultApplication(APP_ID, VER, DESC, ORIGIN + "asd", ROLE,
PERMS, Optional.of(FURL), FEATURES);
PERMS, Optional.of(FURL), FEATURES, APPS);
new EqualsTester().addEqualityGroup(a1, a2)
.addEqualityGroup(a3).addEqualityGroup(a4).testEquals();
}
......
......@@ -32,18 +32,18 @@ public final class ApplicationCodec extends JsonCodec<Application> {
public ObjectNode encode(Application app, CodecContext context) {
checkNotNull(app, "Application cannot be null");
ApplicationService service = context.getService(ApplicationService.class);
ObjectNode result = context.mapper().createObjectNode()
return context.mapper().createObjectNode()
.put("name", app.id().name())
.put("id", app.id().id())
.put("version", app.version().toString())
.put("description", app.description())
.put("origin", app.origin())
.put("permissions", app.permissions().toString())
.put("permissions", app.permissions().toString()) // FIXME: change to an array
.put("featuresRepo", app.featuresRepo().isPresent() ?
app.featuresRepo().get().toString() : "")
.put("features", app.features().toString())
.put("features", app.features().toString()) // FIXME: change to an array
.put("requiredApps", app.requiredApps().toString()) // FIXME: change to an array
.put("state", service.getState(app.id()).toString());
return result;
}
}
......
......@@ -17,6 +17,7 @@ package org.onosproject.common.app;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import org.apache.commons.configuration.ConfigurationException;
......@@ -33,7 +34,6 @@ import org.onosproject.core.Version;
import org.onosproject.security.AppPermission;
import org.onosproject.security.Permission;
import org.onosproject.store.AbstractStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -46,7 +46,6 @@ import java.io.InputStream;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.file.NoSuchFileException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
......@@ -79,6 +78,7 @@ public class ApplicationArchive
private static final String VERSION = "[@version]";
private static final String FEATURES_REPO = "[@featuresRepo]";
private static final String FEATURES = "[@features]";
private static final String APPS = "[@apps]";
private static final String DESCRIPTION = "description";
private static final String ROLE = "security.role";
......@@ -291,8 +291,13 @@ public class ApplicationArchive
URI featuresRepo = featRepo != null ? URI.create(featRepo) : null;
List<String> features = ImmutableList.copyOf(cfg.getString(FEATURES).split(","));
String apps = cfg.getString(APPS, "");
List<String> requiredApps = apps.isEmpty() ?
ImmutableList.of() : ImmutableList.copyOf(apps.split(","));
return new DefaultApplicationDescription(name, version, desc, origin, role,
perms, featuresRepo, features);
perms, featuresRepo, features,
requiredApps);
}
// Expands the specified ZIP stream into app-specific directory.
......@@ -390,7 +395,7 @@ public class ApplicationArchive
// Returns the set of Permissions specified in the app.xml file
private ImmutableSet<Permission> getPermissions(XMLConfiguration cfg) {
List<Permission> permissionList = new ArrayList();
List<Permission> permissionList = Lists.newArrayList();
for (Object o : cfg.getList(APP_PERMISSIONS)) {
String name = (String) o;
......
......@@ -75,7 +75,8 @@ public class SimpleApplicationStore extends ApplicationArchive implements Applic
new DefaultApplication(appId, appDesc.version(),
appDesc.description(), appDesc.origin(),
appDesc.role(), appDesc.permissions(),
appDesc.featuresRepo(), appDesc.features());
appDesc.featuresRepo(), appDesc.features(),
appDesc.requiredApps());
apps.put(appId, app);
states.put(appId, isActive(name) ? INSTALLED : ACTIVE);
// load app permissions
......@@ -117,7 +118,8 @@ public class SimpleApplicationStore extends ApplicationArchive implements Applic
DefaultApplication app =
new DefaultApplication(appId, appDesc.version(), appDesc.description(),
appDesc.origin(), appDesc.role(), appDesc.permissions(),
appDesc.featuresRepo(), appDesc.features());
appDesc.featuresRepo(), appDesc.features(),
appDesc.requiredApps());
apps.put(appId, app);
states.put(appId, INSTALLED);
delegate.notify(new ApplicationEvent(APP_INSTALLED, app));
......
......@@ -212,6 +212,7 @@ public class ApplicationManager
// The following methods are fully synchronized to guard against remote vs.
// locally induced feature service interactions.
// Installs all feature repositories required by the specified app.
private synchronized boolean installAppArtifacts(Application app) throws Exception {
if (app.featuresRepo().isPresent() &&
featuresService.getRepository(app.featuresRepo().get()) == null) {
......@@ -221,6 +222,7 @@ public class ApplicationManager
return false;
}
// Uninstalls all the feature repositories required by the specified app.
private synchronized boolean uninstallAppArtifacts(Application app) throws Exception {
if (app.featuresRepo().isPresent() &&
featuresService.getRepository(app.featuresRepo().get()) != null) {
......@@ -230,6 +232,7 @@ public class ApplicationManager
return false;
}
// Installs all features that define the specified app.
private synchronized boolean installAppFeatures(Application app) throws Exception {
boolean changed = false;
for (String name : app.features()) {
......@@ -246,6 +249,7 @@ public class ApplicationManager
return changed;
}
// Uninstalls all features that define the specified app.
private synchronized boolean uninstallAppFeatures(Application app) throws Exception {
boolean changed = false;
invokeHook(deactivateHooks.get(app.id().name()), app.id());
......
......@@ -15,6 +15,7 @@
*/
package org.onosproject.app.impl;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.junit.After;
import org.junit.Before;
......@@ -138,7 +139,7 @@ public class ApplicationManagerTest {
@Override
public Application create(InputStream appDescStream) {
app = new DefaultApplication(APP_ID, VER, DESC, ORIGIN, ROLE, PERMS,
Optional.of(FURL), FEATURES);
Optional.of(FURL), FEATURES, ImmutableList.of());
state = INSTALLED;
delegate.notify(new ApplicationEvent(APP_INSTALLED, app));
return app;
......@@ -177,6 +178,11 @@ public class ApplicationManagerTest {
state = INSTALLED;
delegate.notify(new ApplicationEvent(APP_DEACTIVATED, app));
}
@Override
public ApplicationId getId(String name) {
return new DefaultApplicationId(0, name);
}
}
private class TestFeaturesService extends FeaturesServiceAdapter {
......
......@@ -17,7 +17,9 @@ package org.onosproject.store.app;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
......@@ -37,6 +39,7 @@ import org.onosproject.common.app.ApplicationArchive;
import org.onosproject.core.Application;
import org.onosproject.core.ApplicationId;
import org.onosproject.core.ApplicationIdStore;
import org.onosproject.core.CoreService;
import org.onosproject.core.DefaultApplication;
import org.onosproject.security.Permission;
import org.onosproject.store.cluster.messaging.ClusterCommunicationService;
......@@ -61,6 +64,8 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Function;
import static com.google.common.collect.Multimaps.newSetMultimap;
import static com.google.common.collect.Multimaps.synchronizedSetMultimap;
import static com.google.common.io.ByteStreams.toByteArray;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.onlab.util.Tools.groupedThreads;
......@@ -115,6 +120,14 @@ public class GossipApplicationStore extends ApplicationArchive
@Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
protected ApplicationIdStore idStore;
// Multimap to track which apps are required by others apps
// app -> { required-by, ... }
// Apps explicitly activated will be required by the CORE app
private final Multimap<ApplicationId, ApplicationId> requiredBy =
synchronizedSetMultimap(newSetMultimap(Maps.newHashMap(), Sets::newHashSet));
private ApplicationId coreAppId;
@Activate
public void activate() {
KryoNamespace.Builder serializer = KryoNamespace.newBuilder()
......@@ -161,6 +174,7 @@ public class GossipApplicationStore extends ApplicationArchive
.withTimestampProvider((k, v) -> clockService.getTimestamp())
.build();
coreAppId = getId(CoreService.CORE_APP_NAME);
log.info("Started");
}
......@@ -174,6 +188,7 @@ public class GossipApplicationStore extends ApplicationArchive
try {
Application app = create(getApplicationDescription(name), false);
if (app != null && isActive(app.id().name())) {
requiredBy.put(app.id(), coreAppId);
activate(app.id(), false);
// load app permissions
}
......@@ -200,7 +215,6 @@ public class GossipApplicationStore extends ApplicationArchive
public void setDelegate(ApplicationStoreDelegate delegate) {
super.setDelegate(delegate);
loadFromDisk();
// executor.schedule(this::pruneUninstalledApps, LOAD_TIMEOUT_MS, MILLISECONDS);
}
@Override
......@@ -229,8 +243,16 @@ public class GossipApplicationStore extends ApplicationArchive
@Override
public Application create(InputStream appDescStream) {
ApplicationDescription appDesc = saveApplication(appDescStream);
if (hasPrerequisites(appDesc)) {
return create(appDesc, true);
}
throw new ApplicationException("Missing dependencies for app " + appDesc.name());
}
private boolean hasPrerequisites(ApplicationDescription app) {
return !app.requiredApps().stream().map(n -> getId(n))
.anyMatch(id -> id == null || getApplication(id) == null);
}
private Application create(ApplicationDescription appDesc, boolean updateTime) {
Application app = registerApp(appDesc);
......@@ -246,35 +268,79 @@ public class GossipApplicationStore extends ApplicationArchive
public void remove(ApplicationId appId) {
Application app = apps.get(appId);
if (app != null) {
uninstallDependentApps(app);
apps.remove(appId);
states.remove(app);
permissions.remove(app);
}
}
// Uninstalls all apps that depend on the given app.
private void uninstallDependentApps(Application app) {
getApplications().stream()
.filter(a -> a.requiredApps().contains(app.id().name()))
.forEach(a -> remove(a.id()));
}
@Override
public void activate(ApplicationId appId) {
activate(appId, coreAppId);
}
private void activate(ApplicationId appId, ApplicationId forAppId) {
requiredBy.put(appId, forAppId);
activate(appId, true);
}
private void activate(ApplicationId appId, boolean updateTime) {
Application app = apps.get(appId);
if (app != null) {
if (updateTime) {
updateTime(appId.name());
}
activateRequiredApps(app);
states.put(app, ACTIVATED);
}
}
// Activates all apps required by this application.
private void activateRequiredApps(Application app) {
app.requiredApps().stream().map(this::getId).forEach(id -> activate(id, app.id()));
}
@Override
public void deactivate(ApplicationId appId) {
deactivateDependentApps(getApplication(appId));
deactivate(appId, coreAppId);
}
private void deactivate(ApplicationId appId, ApplicationId forAppId) {
requiredBy.remove(appId, forAppId);
if (requiredBy.get(appId).isEmpty()) {
Application app = apps.get(appId);
if (app != null) {
updateTime(appId.name());
states.put(app, DEACTIVATED);
deactivateRequiredApps(app);
}
}
}
// Deactivates all apps that require this application.
private void deactivateDependentApps(Application app) {
getApplications().stream()
.filter(a -> states.get(a) == ACTIVATED)
.filter(a -> a.requiredApps().contains(app.id().name()))
.forEach(a -> deactivate(a.id()));
}
// Deactivates all apps required by this application.
private void deactivateRequiredApps(Application app) {
app.requiredApps().stream().map(this::getId).map(this::getApplication)
.filter(a -> states.get(a) == ACTIVATED)
.forEach(a -> deactivate(a.id(), app.id()));
}
@Override
public Set<Permission> getPermissions(ApplicationId appId) {
......@@ -424,6 +490,7 @@ public class GossipApplicationStore extends ApplicationArchive
ApplicationId appId = idStore.registerApplication(appDesc.name());
return new DefaultApplication(appId, appDesc.version(), appDesc.description(),
appDesc.origin(), appDesc.role(), appDesc.permissions(),
appDesc.featuresRepo(), appDesc.features());
appDesc.featuresRepo(), appDesc.features(),
appDesc.requiredApps());
}
}
......
......@@ -62,6 +62,7 @@ public class OnosAppMojo extends AbstractMojo {
private static final String ONOS_APP_NAME = "onos.app.name";
private static final String ONOS_APP_ORIGIN = "onos.app.origin";
private static final String ONOS_APP_REQUIRES = "onos.app.requires";
private static final String JAR = "jar";
private static final String XML = "xml";
......@@ -80,6 +81,7 @@ public class OnosAppMojo extends AbstractMojo {
private String name;
private String origin;
private String requiredApps;
private String version = DEFAULT_VERSION;
private String featuresRepo = DEFAULT_FEATURES_REPO;
private List<String> artifacts;
......@@ -160,6 +162,9 @@ public class OnosAppMojo extends AbstractMojo {
origin = (String) project.getProperties().get(ONOS_APP_ORIGIN);
origin = origin != null ? origin : DEFAULT_ORIGIN;
requiredApps = (String) project.getProperties().get(ONOS_APP_REQUIRES);
requiredApps = requiredApps == null ? "" : requiredApps;
if (appFile.exists()) {
loadAppFile(appFile);
} else {
......@@ -338,6 +343,7 @@ public class OnosAppMojo extends AbstractMojo {
return string == null ? null :
string.replaceAll("\\$\\{onos.app.name\\}", name)
.replaceAll("\\$\\{onos.app.origin\\}", origin)
.replaceAll("\\$\\{onos.app.requires\\}", requiredApps)
.replaceAll("\\$\\{project.groupId\\}", projectGroupId)
.replaceAll("\\$\\{project.artifactId\\}", projectArtifactId)
.replaceAll("\\$\\{project.version\\}", projectVersion)
......
......@@ -16,7 +16,7 @@
-->
<app name="${onos.app.name}" origin="${onos.app.origin}" version="${project.version}"
featuresRepo="mvn:${project.groupId}/${project.artifactId}/${project.version}/xml/features"
features="${project.artifactId}">
features="${project.artifactId}" apps="${onos.app.requires}">
<description>${project.description}</description>
<artifact>mvn:${project.groupId}/${project.artifactId}/${project.version}</artifact>
</app>
......
......@@ -85,19 +85,19 @@ public class ApplicationsResourceTest extends ResourceTest {
private Application app1 =
new DefaultApplication(id1, VER,
"app1", "origin1", ApplicationRole.ADMIN, ImmutableSet.of(), Optional.of(FURL),
ImmutableList.of("My Feature"));
ImmutableList.of("My Feature"), ImmutableList.of());
private Application app2 =
new DefaultApplication(id2, VER,
"app2", "origin2", ApplicationRole.ADMIN, ImmutableSet.of(), Optional.of(FURL),
ImmutableList.of("My Feature"));
ImmutableList.of("My Feature"), ImmutableList.of());
private Application app3 =
new DefaultApplication(id3, VER,
"app3", "origin3", ApplicationRole.ADMIN, ImmutableSet.of(), Optional.of(FURL),
ImmutableList.of("My Feature"));
ImmutableList.of("My Feature"), ImmutableList.of());
private Application app4 =
new DefaultApplication(id4, VER,
"app4", "origin4", ApplicationRole.ADMIN, ImmutableSet.of(), Optional.of(FURL),
ImmutableList.of("My Feature"));
ImmutableList.of("My Feature"), ImmutableList.of());
/**
* Hamcrest matcher to check that an application representation in JSON matches
......