package org.ff4j;
import static org.ff4j.audit.EventConstants.ACTION_CHECK_OFF;
import static org.ff4j.audit.EventConstants.ACTION_CHECK_OK;
import static org.ff4j.audit.EventConstants.SOURCE_JAVA;
/*
* #%L
* ff4j-core
* %%
* Copyright (C) 2013 Ff4J
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import org.ff4j.audit.EventBuilder;
import org.ff4j.audit.EventPublisher;
import org.ff4j.audit.proxy.FeatureStoreAuditProxy;
import org.ff4j.audit.proxy.PropertyStoreAuditProxy;
import org.ff4j.audit.repository.EventRepository;
import org.ff4j.audit.repository.InMemoryEventRepository;
import org.ff4j.cache.FF4JCacheManager;
import org.ff4j.cache.FF4jCacheProxy;
import org.ff4j.conf.XmlConfig;
import org.ff4j.conf.XmlParser;
import org.ff4j.core.Feature;
import org.ff4j.core.FeatureStore;
import org.ff4j.core.FlippingExecutionContext;
import org.ff4j.core.FlippingStrategy;
import org.ff4j.exception.FeatureNotFoundException;
import org.ff4j.property.Property;
import org.ff4j.property.store.InMemoryPropertyStore;
import org.ff4j.property.store.PropertyStore;
import org.ff4j.security.AuthorizationsManager;
import org.ff4j.store.InMemoryFeatureStore;
/**
* Principal class stands as public api to work with FF4J.
*
* <ul>
* <p>
* <li>
* It embeddes a {@link FeatureStore} to record features statused. By default, features are stored into memory but you would like
* to persist them in an external storage (as database) and choose among implementations available in different modules (jdbc,
* mongo, http...).
* </p>
*
* <p>
* <li>It embeddes a {@link AuthorizationsManager} to add permissions and limit usage of features to granted people. FF4J does not
* created roles, it's rely on external security provider as SpringSecurity Apache Chiro.
* </p>
*
* <p>
* <li>It embeddes a {@link EventRepository} to monitoring actions performed on features.
* </p>
*
* </ul>
*
* @author Cedrick Lunven (@clunven)
*/
public class FF4j {
/** Intialisation. */
private final long startTime = System.currentTimeMillis();
/** Version of ff4j. */
private final String version = getClass().getPackage().getImplementationVersion();
/** Source of initialization (JAVA_API, WEBAPI, SSH, CONSOLE...). */
private String source = SOURCE_JAVA;
// -- Stores --
/** Storage to persist feature within {@link FeatureStore}. */
private FeatureStore fstore = new InMemoryFeatureStore();
/** Storage to persist properties within {@link PropertyStore}. */
private PropertyStore pStore = new InMemoryPropertyStore();
/** Do not through {@link FeatureNotFoundException} exception and but feature is required. */
private boolean autocreate = false;
/** Security policy to limit access through ACL with {@link AuthorizationsManager}. */
private AuthorizationsManager authorizationsManager = null;
// -- Audit --
/** Capture informations relative to audit. */
private boolean enableAudit = false;
/** Repository for audit event. */
private EventRepository eventRepository = new InMemoryEventRepository();
/** Event Publisher (threadpool, executor) to send data into {@link EventRepository} */
private EventPublisher eventPublisher = null;
/** This attribute indicates to stop the event publisher. */
private volatile boolean shutdownEventPublisher;
// -- Settings --
/** Post Processing like audit enable. */
private boolean initialized = false;
/** Hold flipping execution context as Thread-safe data. */
private ThreadLocal<FlippingExecutionContext> currentExecutionContext = new ThreadLocal<FlippingExecutionContext>();
/**
* This attribute indicates when call the alter bean throw de {@link InvocationTargetException}
* or the wraps exception thrown by an invoked method or constructor
*/
private boolean alterBeanThrowInvocationTargetException = true;
/**
* Default constructor to allows instantiation through IoC. The created store is an empty {@link InMemoryFeatureStore}.
*/
public FF4j() {
}
/**
* Constructor initializing ff4j with an InMemoryStore
*/
public FF4j(String xmlFile) {
this.fstore = new InMemoryFeatureStore(xmlFile);
this.pStore = new InMemoryPropertyStore(xmlFile);
}
/**
* Constructor initializing ff4j with an InMemoryStore using an InputStream. Simplify integration with Android through
* <code>Asset</code>
*/
public FF4j(InputStream xmlFileResourceAsStream) {
this.fstore = new InMemoryFeatureStore(xmlFileResourceAsStream);
}
/**
* Ask if flipped.
*
* @param featureID
* feature unique identifier.
* @param executionContext
* current execution context
* @return current feature status
*/
public boolean check(String featureID) {
return check(featureID, null);
}
/**
* Elegant way to ask for flipping.
*
* @param featureID
* feature unique identifier.
* @param executionContext
* current execution context
* @return current feature status
*/
public boolean check(String featureID, FlippingExecutionContext executionContext) {
Feature fp = getFeature(featureID);
boolean flipped = fp.isEnable();
// If authorization manager provided, apply security filter
if (flipped && getAuthorizationsManager() != null) {
flipped = isAllowed(fp);
}
// If custom strategy has been defined, delegate flipping to
if (flipped && fp.getFlippingStrategy() != null) {
flipped = fp.getFlippingStrategy().evaluate(featureID, getFeatureStore(), executionContext);
}
// Update current context
currentExecutionContext.set(executionContext);
// Any access is logged into audit system
publishCheck(featureID, flipped);
return flipped;
}
/**
* Send target event to audit if expected.
*
* @param uid
* feature unique identifier
* @param checked
* if the feature is checked or not
*/
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());
}
}
/**
* Overriding strategy on feature.
*
* @param featureID
* feature unique identifier.
* @param executionContext
* current execution context
* @return
*/
public boolean checkOveridingStrategy(String featureID, FlippingStrategy strats) {
return checkOveridingStrategy(featureID, strats, currentExecutionContext.get());
}
/**
* Overriding strategy on feature.
*
* @param featureID
* feature unique identifier.
* @param executionContext
* current execution context
* @return
*/
public boolean checkOveridingStrategy(String featureID, FlippingStrategy strats, FlippingExecutionContext executionContext) {
Feature fp = getFeature(featureID);
boolean flipped = fp.isEnable() && isAllowed(fp);
if (strats != null) {
flipped = flipped && strats.evaluate(featureID, getFeatureStore(), executionContext);
}
publishCheck(featureID, flipped);
return flipped;
}
/**
* Load SecurityProvider roles (e.g : SpringSecurity GrantedAuthorities)
*
* @param featureName
* target name of the feature
* @return if the feature is allowed
*/
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;
}
/**
* Read Features from store.
*
* @return get store features
*/
public Map<String, Feature> getFeatures() {
return getFeatureStore().readAll();
}
/**
* Return all properties from store.
*
* @return
* target property store.
*/
public Map < String, Property<?>> getProperties() {
return getPropertiesStore().readAllProperties();
}
/**
* Enable Feature.
*
* @param featureID
* unique feature identifier.
*/
public FF4j enable(String featureID) {
try {
getFeatureStore().enable(featureID);
} catch (FeatureNotFoundException fnfe) {
if (this.autocreate) {
getFeatureStore().create(new Feature(featureID, true));
} else {
throw fnfe;
}
}
return this;
}
/**
* Enable group.
*
* @param groupName
* target groupeName
* @return current instance
*/
public FF4j enableGroup(String groupName) {
getFeatureStore().enableGroup(groupName);
return this;
}
/**
* Disable group.
*
* @param groupName
* target groupeName
* @return current instance
*/
public FF4j disableGroup(String groupName) {
getFeatureStore().disableGroup(groupName);
return this;
}
/**
* Create new Feature.
*
* @param featureID
* unique feature identifier.
*/
public FF4j createFeature(Feature fp) {
getFeatureStore().create(fp);
return this;
}
/**
* Create new Property.
*
* @param featureID
* unique feature identifier.
*/
public FF4j createProperty(Property<?> prop) {
getPropertiesStore().createProperty(prop);
return this;
}
/**
* Create new Feature.
*
* @param featureID
* unique feature identifier.
*/
public FF4j createFeature(String featureName, boolean enable, String description) {
return createFeature(new Feature(featureName, enable, description));
}
/**
* Create new Feature.
*
* @param featureID
* unique feature identifier.
*/
public FF4j createFeature(String featureName, boolean enable) {
return createFeature(featureName, enable, "");
}
/**
* Create new Feature.
*
* @param featureID
* unique feature identifier.
*/
public FF4j createFeature(String featureName) {
return createFeature(featureName, false, "");
}
/**
* Disable Feature.
*
* @param featureID
* unique feature identifier.
*/
public FF4j disable(String featureID) {
try {
getFeatureStore().disable(featureID);
} catch (FeatureNotFoundException fnfe) {
if (this.autocreate) {
getFeatureStore().create(new Feature(featureID, false));
} else {
throw fnfe;
}
}
return this;
}
/**
* Check if target feature exist.
*
* @param featureId
* unique feature identifier.
* @return flag to check existence of
*/
public boolean exist(String featureId) {
return getFeatureStore().exist(featureId);
}
/**
* The feature will be create automatically if the boolea, autocreate is enabled.
*
* @param featureID
* target feature ID
* @return target feature.
*/
public Feature getFeature(String featureID) {
Feature fp = null;
try {
fp = getFeatureStore().read(featureID);
} catch (FeatureNotFoundException fnfe) {
if (this.autocreate) {
fp = new Feature(featureID, false);
getFeatureStore().create(fp);
} else {
throw fnfe;
}
}
return fp;
}
/**
* Read property in Store
*
* @param featureID
* target feature ID
* @return target feature.
*/
public Property<?> getProperty(String propertyName) {
return getPropertiesStore().readProperty(propertyName);
}
/**
* Read property in Store
*
* @param featureID
* target feature ID
* @return target feature.
*/
public String getPropertyAsString(String propertyName) {
return getProperty(propertyName).asString();
}
/**
* Help to import features.
*
* @param features
* set of features.
* @return
* a reference to this object (builder pattern).
*
* @since 1.6
*/
public FF4j importFeatures(Collection < Feature> features) {
getFeatureStore().importFeatures(features);
return this;
}
/**
* Help to import propertiess.
*
* @param features
* set of features.
* @return
* a reference to this object (builder pattern).
*
* @since 1.6
*/
public FF4j importProperties(Collection < Property<?>> properties) {
if (properties != null) {
for (Property<?> property : properties) {
getPropertiesStore().createProperty(property);
}
}
return this;
}
/**
* Export Feature through FF4J.
*
* @return
* @throws IOException
*/
public InputStream exportFeatures() throws IOException {
return new XmlParser().exportFeatures(getFeatureStore().readAll());
}
/**
* Enable autocreation of features when not found.
*
* @param flag
* target value for autocreate flag
* @return current instance
*/
public FF4j autoCreate(boolean flag) {
setAutocreate(flag);
return this;
}
/**
* Enable autocreation of features when not found.
*
* @param flag
* target value for autocreate flag
* @return current instance
*/
public FF4j autoCreate() {
return autoCreate(true);
}
/**
* Enable auditing of features when not found.
*
* @param flag
* target value for autocreate flag
* @return current instance
*/
public FF4j audit() {
return audit(true);
}
/**
* Enable auditing of features when not found.
*
* @param flag
* target value for autocreate flag
* @return current instance
*/
public FF4j audit(boolean val) {
setEnableAudit(val);
return this;
}
/**
* Delete feature name.
*
* @param fpId
* target feature
*/
public FF4j delete(String fpId) {
getFeatureStore().delete(fpId);
return this;
}
/**
* Delete new Property.
*
* @param featureID
* unique feature identifier.
*/
public FF4j deleteProperty(String propertyName) {
getPropertiesStore().deleteProperty(propertyName);
return this;
}
/**
* Enable a cache proxy.
*
* @param cm
* current cache manager
* @return
* current ff4j bean
*/
public FF4j cache(FF4JCacheManager cm) {
FF4jCacheProxy cp = new FF4jCacheProxy(getFeatureStore(), getPropertiesStore(), cm);
setFeatureStore(cp);
setPropertiesStore(cp);
return this;
}
/**
* Parse configuration file.
*
* @param fileName
* target file
* @return
* current configuration as XML
*/
public XmlConfig parseXmlConfig(String fileName) {
InputStream xmlIN = getClass().getClassLoader().getResourceAsStream(fileName);
if (xmlIN == null) {
throw new IllegalArgumentException("Cannot parse XML file " + fileName + " - file not found");
}
return new XmlParser().parseConfigurationFile(xmlIN);
}
/** {@inheritDoc} */
@Override
public String toString() {
StringBuilder sb = new StringBuilder("{");
long uptime = System.currentTimeMillis() - startTime;
long daynumber = uptime / (1000 * 3600 * 24L);
uptime = uptime - daynumber * 1000 * 3600 * 24L;
long hourNumber = uptime / (1000 * 3600L);
uptime = uptime - hourNumber * 1000 * 3600L;
long minutenumber = uptime / (1000 * 60L);
uptime = uptime - minutenumber * 1000 * 60L;
long secondnumber = uptime / 1000L;
sb.append("\"uptime\":\"");
sb.append(daynumber + " day(s) ");
sb.append(hourNumber + " hours(s) ");
sb.append(minutenumber + " minute(s) ");
sb.append(secondnumber + " seconds\"");
sb.append(", \"autocreate\":" + isAutocreate());
sb.append(", \"version\": \"" + version + "\"");
// Display only if not null
if (getFeatureStore() != null) {
sb.append(", \"featuresStore\":");
sb.append(getFeatureStore().toString());
}
if (getPropertiesStore() != null) {
sb.append(", \"propertiesStore\":");
sb.append(getPropertiesStore().toString());
}
if (getEventRepository() != null) {
sb.append(", \"eventRepository\":");
sb.append(getEventRepository().toString());
}
if (getAuthorizationsManager() != null) {
sb.append(", \"authorizationsManager\":");
sb.append(getAuthorizationsManager().toString());
}
sb.append("}");
return sb.toString();
}
// -------------------------------------------------------------------------
// ------------------- GETTERS & SETTERS -----------------------------------
// -------------------------------------------------------------------------
/**
* NON Static to be use by Injection of Control.
*
* @param fbs
* target store.
*/
public void setFeatureStore(FeatureStore fbs) {
this.fstore = fbs;
}
/**
* Setter accessor for attribute 'autocreate'.
*
* @param autocreate
* new value for 'autocreate '
*/
public void setAutocreate(boolean autocreate) {
this.autocreate = autocreate;
}
/**
* Getter accessor for attribute 'authorizationsManager'.
*
* @return current value of 'authorizationsManager'
*/
public AuthorizationsManager getAuthorizationsManager() {
return authorizationsManager;
}
/**
* Setter accessor for attribute 'authorizationsManager'.
*
* @param authorizationsManager
* new value for 'authorizationsManager '
*/
public void setAuthorizationsManager(AuthorizationsManager authorizationsManager) {
this.authorizationsManager = authorizationsManager;
}
/**
* Getter accessor for attribute 'eventRepository'.
*
* @return current value of 'eventRepository'
*/
public EventRepository getEventRepository() {
return eventRepository;
}
/**
* Setter accessor for attribute 'eventRepository'.
*
* @param eventRepository
* new value for 'eventRepository '
*/
public void setEventRepository(EventRepository eventRepository) {
this.eventRepository = eventRepository;
}
/**
* Setter accessor for attribute 'eventPublisher'.
*
* @param eventPublisher
* new value for 'eventPublisher '
*/
public void setEventPublisher(EventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
/**
* Initialization of background components.
*/
private synchronized void init() {
// Execution Context
FlippingExecutionContext context = new FlippingExecutionContext();
this.currentExecutionContext.set(context);
// Event Publisher
if (eventPublisher == null) {
eventPublisher = new EventPublisher(eventRepository);
this.shutdownEventPublisher = true;
}
// Audit is enabled, proxified stores for auditing
if (isEnableAudit()) {
if (fstore != null && !(fstore instanceof FeatureStoreAuditProxy)) {
this.fstore = new FeatureStoreAuditProxy(this, fstore);
}
if (pStore != null && !(pStore instanceof PropertyStoreAuditProxy)) {
this.pStore = new PropertyStoreAuditProxy(this, pStore);
}
} else {
// Audit is disabled but could have been enabled before... removing PROXY if relevant
if (fstore != null && fstore instanceof FeatureStoreAuditProxy) {
this.fstore = ((FeatureStoreAuditProxy) fstore).getTarget();
}
if (pStore != null && pStore instanceof PropertyStoreAuditProxy) {
this.pStore = ((PropertyStoreAuditProxy) pStore).getTarget();
}
}
// Flag as OK
this.initialized = true;
}
/**
* Create tables/collections/columns in DB (if required).
*/
public void createSchema() {
if (null != getFeatureStore()) {
getFeatureStore().createSchema();
}
if (null != getPropertiesStore()) {
getPropertiesStore().createSchema();
}
if (null != getEventRepository()) {
getEventRepository().createSchema();
}
}
/**
* Access store as static way (single store).
*
* @return current store
*/
public FeatureStore getFeatureStore() {
if (!initialized) {
init();
}
return fstore;
}
/**
* Getter accessor for attribute 'eventPublisher'.
*
* @return current value of 'eventPublisher'
*/
public EventPublisher getEventPublisher() {
if (!initialized) {
init();
}
return eventPublisher;
}
/**
* Getter accessor for attribute 'pStore'.
*
* @return
* current value of 'pStore'
*/
public PropertyStore getPropertiesStore() {
if (!initialized) {
init();
}
return pStore;
}
/**
* Initialize flipping execution context.
*
* @return
* get current context
*/
public FlippingExecutionContext getCurrentContext() {
if (!initialized) {
init();
}
if (null == this.currentExecutionContext.get()) {
this.currentExecutionContext.set(new FlippingExecutionContext());
}
return this.currentExecutionContext.get();
}
/**
* Getter accessor for attribute 'autocreate'.
*
* @return current value of 'autocreate'
*/
public boolean isAutocreate() {
return autocreate;
}
/**
* Getter accessor for attribute 'startTime'.
*
* @return
* current value of 'startTime'
*/
public long getStartTime() {
return startTime;
}
/**
* Getter accessor for attribute 'version'.
*
* @return
* current value of 'version'
*/
public String getVersion() {
return version;
}
/**
* Setter accessor for attribute 'pStore'.
* @param pStore
* new value for 'pStore '
*/
public void setPropertiesStore(PropertyStore pStore) {
this.pStore = pStore;
}
/**
* Clear context.
*/
public void removeCurrentContext() {
this.currentExecutionContext.remove();
}
/**
* Getter accessor for attribute 'enableAudit'.
*
* @return
* current value of 'enableAudit'
*/
public boolean isEnableAudit() {
return enableAudit;
}
/**
* Setter accessor for attribute 'enableAudit'.
*
* @param enableAudit
* new value for 'enableAudit '
*/
public void setEnableAudit(boolean enableAudit) {
this.enableAudit = enableAudit;
// if you disable the audit : the auditProxy must be destroy and use targets
initialized = false;
}
/**
* Required for spring namespace and 'fileName' attribut on ff4j tag.
*
* @param fname
* target name
*/
public void setFileName(String fname) { /** empty setter for Spring framework */ }
public void setAuthManager(String mnger) { /** empty setter for Spring framework */}
/**
* Shuts down the event publisher if we actually started it (As opposed to
* having it dependency-injected).
*/
public void stop() {
if (this.eventPublisher != null && this.shutdownEventPublisher) {
this.eventPublisher.stop();
}
}
/**
* Getter accessor for attribute 'source'.
*
* @return
* current value of 'source'
*/
public String getSource() {
return source;
}
/**
* Reach concrete implementation of the featureStore.
*
* @return
*/
public FeatureStore getConcreteFeatureStore() {
return getConcreteFeatureStore(getFeatureStore());
}
/**
* Reach concrete implementation of the propertyStore.
*
* @return
*/
public PropertyStore getConcretePropertyStore() {
return getConcretePropertyStore(getPropertiesStore());
}
/**
* try to fetch CacheProxy (cannot handled proxy CGLIB, ASM or any bytecode manipulation).
*
* @return
*/
public FF4jCacheProxy getCacheProxy() {
FeatureStore fs = getFeatureStore();
// Pass through audit proxy if exists
if (fs instanceof FeatureStoreAuditProxy) {
fs = ((FeatureStoreAuditProxy) fs).getTarget();
}
if (fs instanceof FF4jCacheProxy) {
return (FF4jCacheProxy) fs;
}
return null;
}
/**
* Return concrete implementation.
*
* @param fs
* current featureStore
* @return
* target featureStore
*/
private FeatureStore getConcreteFeatureStore(FeatureStore fs) {
if (fs instanceof FeatureStoreAuditProxy) {
return getConcreteFeatureStore(((FeatureStoreAuditProxy) fs).getTarget());
} else if (fs instanceof FF4jCacheProxy) {
return getConcreteFeatureStore(((FF4jCacheProxy) fs).getTargetFeatureStore());
}
return fs;
}
/**
* Return concrete implementation.
*
* @param fs
* current propertyStoyre
* @return
* target propertyStoyre
*/
private PropertyStore getConcretePropertyStore(PropertyStore ps) {
if (ps instanceof PropertyStoreAuditProxy) {
return getConcretePropertyStore(((PropertyStoreAuditProxy) ps).getTarget());
} else if (ps instanceof FF4jCacheProxy) {
return getConcretePropertyStore(((FF4jCacheProxy) ps).getTargetPropertyStore());
}
return ps;
}
/**
* Enable Alter bean Throw InvocationTargetException, when enabled
* the alter bean method always throw {@link InvocationTargetException}
*/
public void enableAlterBeanThrowInvocationTargetException() {
this.alterBeanThrowInvocationTargetException = true;
}
/**
* Disable Alter bean Throw InvocationTargetException, when disabled
* the alter bean method always throw the exception cause.
*/
public void disableAlterBeanThrowInvocationTargetException() {
this.alterBeanThrowInvocationTargetException = false;
}
/**
* Getter accessor for attribute 'alterBeanThrowInvocationTargetException'.
*
* @return
* current value of 'alterBeanThrowInvocationTargetException'
*/
public boolean isAlterBeanThrowInvocationTargetException() {
return alterBeanThrowInvocationTargetException;
}
}