Skip to content

Core Concepts

pazkooda edited this page Apr 24, 2018 · 20 revisions

Feature

The Feature term is used to represent any functionality or treatment. of an application. It is identified by an unique identifier (uid). As the feature toggle purpose is to enable and disable features at runtime, you could expect that each feature will get a state.

FF4J add a few more attributes like a short description, an optional groupName (see FeatureGroup), an optional access control list (see Permissions), a flipping strategy (see FlippingStrategy) but also the capacity to add your own attributes through customProperties.

// Simplest declaration
Feature f1 = new Feature("f1");

// Declare with description and initial state
Feature f2 = new Feature("f2", false, "sample description");
        
// Illustrate ACL & Group
Set < String > permission = new HashSet<String>();
permission.add("BETA-TESTER");
permission.add("VIP");
Feature f3 = new Feature("f3", false, "sample description", "GROUP_1", permission);
        
// Custom properties
Feature f4 = new Feature("f4");
f4.addProperty(new PropertyString("p1", "v1"));
f4.addProperty(new PropertyDouble("pie", Math.PI));
f4.addProperty(new PropertyInt("myAge", 12));

// Flipping Strategy
Feature f5 = new Feature("f5");
Calendar nextReleaseDate = Calendar.getInstance();
nextReleaseDate.set(Calendar.MONTH, Calendar.SEPTEMBER);
nextReleaseDate.set(Calendar.DAY_OF_MONTH, 1);
f5.setFlippingStrategy(new ReleaseDateFlipStrategy(nextReleaseDate.getTime()));
Feature f6 = new Feature("f6");
f6.setFlippingStrategy(new DarkLaunchStrategy(0.2d));        
Feature f7 = new Feature("f7");
f7.setFlippingStrategy(new WhiteListStrategy("localhost"));

FeatureStore

The FeatureStore is the persistent unit to store the features with their attributes and status. It proposes a set of CRUD operations to administrate features but also groups of features and permissions of features. They are a dozen implementations for different technologies as detailed here.

InMemoryFeatureStore fStore = new InMemoryFeatureStore();
// Operations on features
fStore.create(f5);
fStore.exist("f1");
fStore.enable("f1");

// Operations on permissions
fStore.grantRoleOnFeature("f1","BETA");

// Operation on groups  
fStore.addToGroup("f1", "g1");      
fStore.enableGroup("g1");
Map < String, Feature > groupG1 = fStore.readGroup("g1");

// Read all informations
Map < String, Feature > mapOfFeatures = fStore.readAll();

Property

A Property is an entity representing any kind of value or configuration. It has an unique name and a value. This value can be of any type and this the reason why, in ff4j, we chose to implement it with generics Property<?>. There is a Property implementation for each Java Raw Type as shown in the example below. There are a couple of others attributes like a description and an optional set of valid values (’fixedValues’).

PropertyBigDecimal p01 = new PropertyBigDecimal();
PropertyBigInteger p02 = new PropertyBigInteger("d2", new BigInteger("1"));
PropertyBoolean    p03 = new PropertyBoolean("d2", true);
PropertyByte       p04 = new PropertyByte("d2", "1");
PropertyCalendar   p05 = new PropertyCalendar("d2", "2015-01-02 13:00");
PropertyDate       p06 = new PropertyDate("d2", "2015-01-02 13:00:00");
PropertyDouble     p07 = new PropertyDouble("d2", 1.2);
PropertyFloat      p08 = new PropertyFloat("d2", 1.1F);
PropertyInt 	   p09 = new PropertyInt("d2", 1);
PropertyLogLevel   p10 = new PropertyLogLevel("DEBUG");
PropertyLong       p11 = new PropertyLong("d2", 1L);
PropertyShort      p12 = new PropertyShort("d2", new Short("1"));
PropertyString     p13 = new PropertyString("p1");

As it uses generics, it's easy to create some custom Property like this :

import org.ff4j.property.Property;
import org.ff4j.test.property.CardinalPoint.Point;

public class CardinalPoint extends Property<Point> {

    private static final long serialVersionUID = 1792311055570779010L;

    public static enum Point {NORTH, SOUTH, EAST, WEST};
    
    public CardinalPoint(String uid, Point lvl) {
        super(uid, lvl, Point.values());
    }
    
    /** {@inheritDoc} */
    public Point fromString(String v) { return Point.valueOf(v); } 
    
    public void north() { setValue(Point.NORTH); }
    public void south() { setValue(Point.SOUTH); }  
    public void east()  { setValue(Point.EAST);  } 
    public void west()  { setValue(Point.WEST);  }    
}

Note : There is no JSR for properties at the moment. ’commons-configuration’ proposes a definition that has been also used within ’Archaius’. The model of ’ff4j’ is more broad but you can work with commons-configurations.

PropertyStore

The PropertyStore

PropertyStore pStore = new InMemoryPropertyStore();
        
// CRUD
pStore.existProperty("a");
pStore.createProperty(new PropertyDate("a", new Date()));
Property<Date> pDate = (Property<Date>) pStore.readProperty("a");
pDate.setValue(new Date());
pStore.updateProperty(pDate);
pStore.deleteProperty("a");
        
// Several
pStore.clear();
pStore.readAllProperties();
pStore.listPropertyNames();

In the same way as the FeatureStore you shouldn't have to work directly with the PropertyStore class, you should only use FF4J. Yet, to access propertyStore, you can do the following :

// Access Property Store (with all its proxy : Audit, Cache, AOP....)
PropertyStore pStore1 = ff4j.getPropertiesStore();

// Access concrete class and implementation of the property store
PropertyStore pStore2 = ff4j.getConcretePropertyStore();

Please note that some functions of propertyStore are also expose to FF4J for syntactic sugar :

ff4j.getProperties();
ff4j.createProperty(new PropertyString("p1", "v1"));
ff4j.getProperty("p1");
ff4j.deleteProperty("p1");

FF4J Architecture Overview

The FF4j framework is designed for you to use the class ’org.ff4j.FF4j’ only. Nevertheless seeing what happens behind the scene is mandatory to understand all the implemented capabilities.

  • FeatureStore and PropertyStore are the interfaces to perform CRUD operations on Feature and Properties respectively. There are several implementations of those 2 interfaces to be able to choose the storage technology you will use among : in memory, database, cache, or NoSQL.

  • EventRepository is an interface to save and perform searches on monitoring events. There are several implementations available for this interface. To ensure better performance, writing monitoring events is done asynchronously. The EventPublisher push events into a blocking queue where dedicated threads will invoke the 'save()' method of EventRepository and persist events.

  • If the flag ’audit’ is set to true in the ff4j class, the stores are wrapped with proxies FeatureStoreAuditProxy for features and PropertyStoreAuditProxy for properties. Each operation will raise an Event to be published to EventRepository through the EventPublisher. The check operation of FF4j is also wrapped in order to track feature usage.

// Publish feature usage to repository
private void publishCheck(String uid, boolean checked) {
   if (isEnableAudit()) {
     getEventPublisher().publish(new EventBuilder(this)
                                      .feature(uid)
                                      .action(checked ? ACTION_CHECK_OK : ACTION_CHECK_OFF)
                                      .build());
   }
}

// PropertyStoreAuditProxy : Publish create operation to repository
public  < T > void createProperty(Property<T> prop) {
	long start = System.nanoTime();
    target.createProperty(prop);
    ff4j.getEventPublisher().publish(new EventBuilder(ff4j)
                    .action(ACTION_CREATE)
                    .property(prop.getName())
                    .value(prop.asString())
                    .duration(System.nanoTime() - start)
                    .build());
}

FF4jCacheProxy is meant to be use with slow storage technology (database, http-client..). It will wrap the stores to implement a cache-aside mechanism : Features and Properties are persisted into cache technology to limit overhead of fetching values each time.

This proxy rely on a FF4JCacheManager. There are several implementations for this cache as well. To ensure consistency between multiple nodes of a cluster it's recommended to use a distributed cache provider like Terracotta, HazelCast or Redis (even if eh-cache is available).

public Feature read(String featureUid) {
	Feature fp = getCacheManager().getFeature(featureUid);
    // not in cache but may has been created from now
    if (null == fp) {
    	fp = getTargetFeatureStore().read(featureUid);
        getCacheManager().putFeature(fp);
    }
    return fp;
}

public void delete(String featureId) {
	// Access target store
    getTargetFeatureStore().delete(featureId);
    // even is not present, evict won't failed
    getCacheManager().evictFeature(featureId);
}

AuthorizationsManager handle permissions on Features. As detailed [here] each ’Feature’ may define a set of roles and only people with at least one of this role could use the feature. The FF4J framework does not create roles, it relies on dedicated technology like Spring Security or Apache Shiro.

public boolean isAllowed(Feature featureName) {
    // No authorization manager, returning always true
    if (getAuthorizationsManager() == null) {
    	return true;
    }
    // if no permissions, the feature is public
    if (featureName.getPermissions().isEmpty()) {
    	return true;
    }
    Set<String> userRoles = getAuthorizationsManager().getCurrentUserPermissions();
    for (String expectedRole : featureName.getPermissions()) {
        if (userRoles.contains(expectedRole)) {
            return true;
        }
    }
    return false;
}

FF4J Usage

Init

To work with FF4j you have to define the org.ff4j.FF4j bean and initialize stores.

  • If a proxy is not explicitly declared it won't be enabled (Cache, Audit)
  • If stores are not explicitly defined, ff4j will use in-memory implementations (features, properties, events)

Fortunately they is a constructor to import Feature and Properties into In-Memory test. Here is the basic usage:

FF4j ff4j = new FF4j("ff4j.xml");

It can be more advanced like below. Most of the time, this bean will be initialized through Inversion of Control with Spring for instance.

// Default constructor
FF4j ff4j = new FF4j();
        
// Initialized stores with JDBC
BasicDataSource dbcpDataSource = new BasicDataSource();
dbcpDataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dbcpDataSource.setUsername("sa");
dbcpDataSource.setPassword("");
dbcpDataSource.setUrl("jdbc:hsqldb:mem:.");

ff4j.setFeatureStore(new JdbcFeatureStore(dbcpDataSource));
ff4j.setPropertiesStore(new JdbcPropertyStore(dbcpDataSource));
ff4j.setEventRepository(new JdbcEventRepository(dbcpDataSource));
                
// Enable Audit Proxy
ff4j.audit();
        
// Enable Cache Proxy
ff4j.cache(new InMemoryCacheManager());
        
// Explicite import
XmlConfig xmlConfig = ff4j.parseXmlConfig("ff4j.xml");
ff4j.getFeatureStore().importFeatures(xmlConfig.getFeatures().values());
ff4j.getPropertiesStore().importProperties(xmlConfig.getProperties().values());

Default Usage

Once the bean FF4J is created you will use the method check() to test if a feature is toggle ON or not

FF4j ff4j = new FF4j("ff4j.xml");
if (ff4j.check("foo") {
 // do something
}

Autocreate mode

@Test
public void createFeatureDynamically() {
 // Given : Initialize as empty store
 FF4j ff4j = new FF4j();
 // When: Dynamically register new features
 ff4j.create("f1").enable("f1");
 // Then
 assertTrue(ff4j.exist("f1")); 
 assertTrue(ff4j.check("f1"));
}

The default behaviour of the check(String featureName) method if a feature does not exist is to raise a FeatureNotFoundException but you can override by setting the autoCreate flag as true : if feature is not found it will be created but toggle OFF.

@Test(expected = FeatureNotFoundException.class)
public void readFeatureNotFound() {
  // Given
  FF4j ff4j = new FF4j();
  // When
  ff4j.getFeature("i-dont-exist");
  // Then, expect error...
}

@Test
public void readFeatureNotFoundAutoCreate() {
  // Given
  FF4j ff4j = new FF4j();
  ff4j.autoCreate(true);
  assertFalse(ff4j.exist("foo"));
  // When
  ff4j.check("foo");
  // Then
  assertTrue(ff4j.exist("foo"));
  assertFalse(ff4j.check("foo"));
}

Permissions and Security

Overview

You may have to enable a feature only for a subset of your users. They are belong to a dedicated group or get a dedicated profile. With the Canary Release pattern for instance, the feature could be activated only for beta-tester.

ff4j does not provide any users/groups definition system but, instead, leverage on existing one like Spring Security or Apache Shiro. A set of permissions is defined for each feature but the permissions must already exists in the external security provider. Permissions will be checked if, and only if, the feature is enabled.

[DIAGRAM]

AuthorizationManager

This is the class where ff4j evaluates users permissions against granted list at feature level. An implementation is available out-of-the-box to work with Spring security framework. There are 2 methods to implements. The first one is retrieving current user profiles (to be tested against features ACL), and the second will return a union of all permissions available within the system. It's used in the administration console to display permissions as an editable list.

[DIAGRAM]

Sample code

In this sample we will create a custom implementation of AuthorizationManager which keep the list of permissions in Memory.

• There is no new extra required dependency to implement the AuthorizationManager is in the ff4j-core.jar file. Here is a sample implementation.

public class CustomAuthorizationManager implements AuthorizationsManager {
  public static ThreadLocal<String> currentUserThreadLocal = new ThreadLocal<String>();
  private static final Map<String, Set<String>> permissions = new HashMap<String, Set<String>>();
  
  static {
    permissions.put("userA", new HashSet<String>(Arrays.asList("user", "admin", "beta")));
    permissions.put("userB", new HashSet<String>(Arrays.asList("user")));
    permissions.put("userC", new HashSet<String>(Arrays.asList("user", "beta")));
  }

  /** {@inheritDoc} */
  @Override
  public Set<String> getCurrentUserPermissions() { 
    String currentUser = currentUserThreadLocal.get();
    return permissions.containsKey(currentUser) ? permissions.get(currentUser) : new HashSet<String>();
  }
  
  /** {@inheritDoc} */
  @Override
  public Set<String> listAllPermissions() {
    Set<String> allPermissions = new HashSet<String>();
    for (Set<String> subPersmission : permissions.values()) {
      allPermissions.addAll(subPersmission);
    }
    return allPermissions;
  }
}

• Create a ff4j.xml file with dedicated roles. A user will be able to use the sayHello feature it's enabled and if he has the permission admin. In the same way a user can use sayGoodBye if, and only if, he has the beta OR the user permission.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration>
<features>

 <feature uid="sayHello" description="my first feature" enable="true">
 <security>
  <role name="admin" />
 </security>
</feature>

<feature uid="sayGoodBye" description="null" enable="true">
 <security>
  <role name="beta" />
  <role name="user" />
 </security>
</feature>

</features>

• Here is the unit test to illustrate :

@Test
public void sampleSecurityTest() {

 // Create FF4J
 FF4j ff4j = new FF4j("ff4j-security.xml");

 // Add the Authorization Manager Filter
 AuthorizationsManager authManager = new CustomAuthorizationManager();
 ff4j.setAuthorizationsManager(authManager);

 // Given : Feature exist and enable 
 assertTrue(ff4j.exist("sayHello"));
 assertTrue(ff4j.getFeature("sayHello").isEnable());

 // Unknow user does not have any permission => check is false
 CustomAuthorizationManager.currentUserThreadLocal.set("unknown-user");  
 System.out.println(authManager.getCurrentUserPermissions());
 assertFalse(ff4j.check("sayHello"));

 // userB exist but he has not role Admin
 CustomAuthorizationManager.currentUserThreadLocal.set("userB");
 System.out.println(authManager.getCurrentUserPermissions());
 assertFalse(ff4j.check("sayHello"));

 // userA is admin
 CustomAuthorizationManager.currentUserThreadLocal.set("userA");
 System.out.println(authManager.getCurrentUserPermissions());
 assertTrue(ff4j.check("sayHello"));
}

Working with Spring Security

Even if creating a custom AuthorizationManager is possible, you may want to use a well defined security framework such as Spring Security. The support of the framework is provided out-of-the-box

• Add the following dependency to your pom.xml file.

<dependency>
 <groupId>org.ff4j</groupId>
 <artifactId>ff4j-aop</artifactId>
 <version>${ff4j.version}</version>
</dependency>

• Define a spring security UserDetails implementation with the following applicationContext-security.xml file.

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">

 <bean id="ff4j" class="org.ff4j.FF4j" >
   <property name="store"	ref="ff4j.store.inmemory" />
   <property name="authorizationsManager" ref="ff4j.authorizationManager.spring" />
 </bean>

 <bean id="ff4j.store.inmemory" class="org.ff4j.store.InMemoryFeatureStore" >
  <property name="location" value="ff4j-security.xml" />
 </bean>

 <bean id="ff4j.authorizationManager.spring" class="org.ff4j.security.SpringSecurityAuthorisationManager">
 </bean>

</beans>

• The ff4j-security.xml file has not changed from last sample

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration> <features>
<feature uid="sayHello" description="my first feature" enable="true">
 <security>
  <role name="admin" />
 </security>
</feature>

<feature uid="sayGoodBye" description="null" enable="true">
 <security>
  <role name="beta" /> <role name="user" />
 </security>
</feature>

</features>

• Create the following test. It instanciates a spring security context and authenticate a 'userA' with the permission 'beta'.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:*applicationContext-security.xml"})
public class SampleSpringSecurityTest {

 @Autowired
 private FF4j ff4j;

 /** Security context. */
 private SecurityContext securityCtx;

 @Before
 public void setUp() throws Exception {
  securityCtx = SecurityContextHolder.getContext();
  
  // UserA got the roles : beta, user, admin
  List<GrantedAuthority> listOfRoles = new ArrayList<GrantedAuthority>();
  listOfRoles.add(new SimpleGrantedAuthority("beta"));
  User userA = new User("userA", "passwdA", listOfRoles);
  String userName = userA.getUsername();
  String passwd = userA.getPassword();
  UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userName, passwd, listOfRoles);
  token.setDetails(userA);
  
  // Create a security context with
  SecurityContext context = new SecurityContextImpl(); context.setAuthentication(token);
  SecurityContextHolder.setContext(context);
 }

 @Test
 public void testIsAuthenticatedAndAuthorized() {
  // Given userA is authenticated in Spring
  Authentication auth = SecurityContextHolder.getContext().getAuthentication();
  Assert.assertTrue(auth.isAuthenticated());
   
  // UserA has not expected role 'admin' 
  assertTrue(ff4j.exist("sayHello"));
  assertTrue(ff4j.getFeature("sayHello").isEnable());
  assertTrue(ff4j.getFeature("sayHello").getPermissions().contains("admin"));
  assertFalse(ff4j.check("sayHello"));
  
  // UserA has expected role 'beta' 
  assertTrue(ff4j.exist("sayGoodBye"));
  assertTrue(ff4j.getFeature("sayGoodBye").isEnable());
  assertTrue(ff4j.getFeature("sayGoodBye").getPermissions().contains("beta"));
  assertTrue(ff4j.check("sayGoodBye"));
 }

 @After
 public void tearDown() {
   SecurityContextHolder.setContext(securityCtx);
 }
}

Flipping Strategy

FlippingStrategy are predicates implementing custom logic to evaluate if a feature should be considered as toggled or not. They will be evaluate if, and only if, the feature is already enabled. At any case it won't change the status of the feature. They are the Check Rules box in the following flowchart:

Here is a sample code to understand the logic:

public class YaFF4jTest{

    @Test
    public void sampleFlippingStrategy() {
        // Given
        //default, in memory and empty.
        FF4j ff4j  = new FF4j();
        Feature f1 = new Feature("f1", true);
        ff4j.getFeatureStore().create(f1);
        //The feature  is enabled, no flipping strategy
        Assert.assertTrue(ff4j.check("f1"));
        
        // Let's add a flipping strategy (yyyy-MM-dd-HH:mm)
        f1.setFlippingStrategy(new ReleaseDateFlipStrategy("2027-03-01-00:00"));
        ff4j.getFeatureStore().update(f1);
        // Even is feature is enabledn as strategy is false...
        Assert.assertFalse(ff4j.check("f1"));
    }
}

This class is set up with a set of initial parameters provided through init(..) method of with the constructor. Those initial parameters should be hold in the class as there is the getInitParam() method to implement.

The evaluate() method expects a FlippingExecutionContext which hold parameters as key/value pairs and provides you the feature name and a reference to the feature store. We provide the superclass AbstractFlipStrategy to ease implementations.

Here a full sample: ReleaseDateFlipStrategy

To see samples and all implementations check here.

You can’t perform that action at this time.