// BlogBridge -- RSS feed reader, manager, and web based service
// Copyright (C) 2002-2006 by R. Pito Salas
//
// This program is free software; you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software Foundation;
// either version 2 of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with this program;
// if not, write to the Free Software Foundation, Inc., 59 Temple Place,
// Suite 330, Boston, MA 02111-1307 USA
//
// Contact: R. Pito Salas
// mailto:pitosalas@users.sourceforge.net
// More information: about BlogBridge
// http://www.blogbridge.com
// http://sourceforge.net/projects/blogbridge
//
// $Id: FeatureManager.java,v 1.17 2008/03/26 13:48:49 spyromus Exp $
//
package com.salas.bb.core;
import com.salas.bb.service.ServerService;
import com.salas.bb.service.ServicePreferences;
import com.salas.bb.utils.Constants;
import com.salas.bb.utils.DateUtils;
import com.salas.bb.utils.StringUtils;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.*;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
/**
* Features manager holds information about all available features and fetches
* the updates from the service.
*/
public final class FeatureManager implements Runnable
{
private static final Logger LOG = Logger.getLogger(FeatureManager.class.getName());
protected static final long UPDATE_PERIOD_SEC = System.getProperty("fm.debug") != null ? 10 : 3600;
static final String KEY_SYNCHRONIZATIONS = "fm.synchronizations";
static final String KEY_LAST_SYNC_DATE = "fm.lastSyncDate";
// --- Default values for features
private static final String DEFAULT_PLAN_NAME = "Free";
private static final Date DEFAULT_PLAN_EXP_DATE = null;
private static final int DEFAULT_PLAN_PERIOD_MONTHS = 0;
private static final int DEFAULT_PLAN_PRICE = 0;
private static final boolean DEFAULT_PLAN_TRIAL = false;
private static final int DEFAULT_PUB_LIMIT = 2;
private static final int DEFAULT_SUB_LIMIT = 300;
private static final int DEFAULT_SYN_LIMIT = 2;
private static final boolean DEFAULT_PTB_ENABLED = false;
private static final boolean DEFAULT_PTB_ADVANCED = false;
private static final boolean DEFAULT_SF_DEDUPLICATION = false;
private static final boolean DEFAULT_AUTO_SAVING = false;
private static final boolean DEFAULT_SENTIMENTS_ENABLED = false;
// --- Feature keys
private static final String FEAT_NAME = "_name";
private static final String FEAT_HASH = "_hash";
private static final String FEAT_EXP_DATE = "_exp_date";
private static final String FEAT_PRICE = "_price";
private static final String FEAT_PERIOD_MONTHS = "_period_months";
private static final String FEAT_IS_TRIAL = "_is_trial";
private static final String FEAT_PTB_ENABLED = "ptb-enabled";
private static final String FEAT_PTB_ADVANCED = "ptb-advanced";
private static final String FEAT_PUB_LIMIT = "pub-limit";
private static final String FEAT_SUB_LIMIT = "sub-limit";
private static final String FEAT_SYN_LIMIT = "syn-limit";
private static final String FEAT_SF_DEDUPLICATION = "sf-deduplication";
private static final String FEAT_AUTO_SAVING = "auto-saving";
private static final String FEAT_SA_ENABLED = "sa-enabled"; // Sentiment analysis
// --- Feature property names
public static final String PROP_PUBLICATION_LIMIT = "publicationLimit";
public static final String PROP_SUBSCRIPTION_LIMIT = "subscriptionLimit";
public static final String PROP_SYNCHRONIZATION_LIMIT = "synchronizationLimit";
public static final String PROP_PTB_ENABLED = "ptbEnabled";
public static final String PROP_PTB_ADVANCED = "ptbAdvanced";
public static final String PROP_SF_DEDUPLICATION = "sfDeduplication";
public static final String PROP_AUTO_SAVING = "autoSaving";
public static final String PROP_SENTIMENTS_ENABLED = "sentimentsEnabled";
// --- Preferences keys
private static final String PREFS_KEY = "plan-features";
// --- Current plan details
private String planHash = "";
private String planName = DEFAULT_PLAN_NAME;
private Date planExpirationDate = DEFAULT_PLAN_EXP_DATE;
private int planPeriodMonths = DEFAULT_PLAN_PERIOD_MONTHS;
private float planPrice = DEFAULT_PLAN_PRICE;
private boolean planTrial = DEFAULT_PLAN_TRIAL;
// --- Features
private int publicationLimit = DEFAULT_PUB_LIMIT;
private int subscriptionLimit = DEFAULT_SUB_LIMIT;
private int synchronizationLimit = DEFAULT_SYN_LIMIT;
private boolean ptbEnabled = DEFAULT_PTB_ENABLED;
private boolean ptbAdvanced = DEFAULT_PTB_ADVANCED;
private boolean sfDeduplication = DEFAULT_SF_DEDUPLICATION;
private boolean autoSaving = DEFAULT_AUTO_SAVING;
private boolean sentimentsEnabled = DEFAULT_SENTIMENTS_ENABLED;
// --- Misc
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
private static final Object SYNCS_LOCK = new Object();
private final Preferences appPrefs;
private ServicePreferences servicePrefs;
/**
* Creates a feataure manager and loads features from the preferences.
*
* @param appPrefs preferences.
*/
public FeatureManager(Preferences appPrefs)
{
this.appPrefs = appPrefs;
loadFeatures();
}
/**
* Sets service preferences.
*
* @param prefs preferences.
*/
public void setServicePreferences(ServicePreferences prefs)
{
this.servicePrefs = prefs;
}
/**
* Invoked to update the features.
*/
public void run()
{
update();
}
/**
* Checks if there's something changed on the service with the feature set
* and update it if there's.
*/
void update()
{
if (servicePrefs != null)
{
String email = servicePrefs.getEmail();
String password = servicePrefs.getPassword();
if (StringUtils.isNotEmpty(email) && StringUtils.isNotEmpty(password))
{
// Ping the service for hash
String newHash = ServerService.getPlanHash(email, password);
// Check if the hash matches our current version
// If it isn't, update the features map and save to the preferences
if (newHash != null && !newHash.equals(planHash))
{
Map<String, String> features = ServerService.getPlanFeatures(email, password);
if (features != null)
{
if (isValid(features))
{
// Store the new hash among features
features.put(FEAT_HASH, newHash);
parseNewFeatures(features);
storeFeatures(features);
}
}
}
} else if (!DEFAULT_PLAN_NAME.equals(getPlanName()))
{
revertToDefault();
}
}
if (isExpired()) revertToDefault();
}
/**
* Checks if current plan is expired.
*
* @return <code>TRUE</code> if the plan is expired.
*/
boolean isExpired()
{
long thisTimeLastWeek = System.currentTimeMillis() - Constants.MILLIS_IN_WEEK;
return planExpirationDate != null && planExpirationDate.before(new Date(thisTimeLastWeek));
}
/**
* Reverts the features to default.
*/
private void revertToDefault()
{
// We intentionally do not revert the hash to avoid continuous updating
// When the plan will be update on the server side so it's no longer expired
// the hash will become different and the updates will be taken.
setPlanName(DEFAULT_PLAN_NAME);
setPlanExpirationDate(DEFAULT_PLAN_EXP_DATE);
setPlanPeriodMonths(DEFAULT_PLAN_PERIOD_MONTHS);
setPlanPrice(DEFAULT_PLAN_PRICE);
setPlanTrial(DEFAULT_PLAN_TRIAL);
setPtbEnabled(DEFAULT_PTB_ENABLED);
setPtbAdvanced(DEFAULT_PTB_ADVANCED);
setPublicationLimit(DEFAULT_PUB_LIMIT);
setSubscriptionLimit(DEFAULT_SUB_LIMIT);
setSynchronizationLimit(DEFAULT_SYN_LIMIT);
setSfDeduplication(DEFAULT_SF_DEDUPLICATION);
setSentimentsEnabled(DEFAULT_SENTIMENTS_ENABLED);
}
/**
* Parses the map of new features, validates it and updates current features if everything seems OK.
*
* @param features new features.
*/
private void parseNewFeatures(Map<String, String> features)
{
// Read the plan features
setPlanHash(features.get(FEAT_HASH));
setPlanName(features.get(FEAT_NAME));
setPlanExpirationDate(getDate(features, FEAT_EXP_DATE));
setPlanPrice(getFloat(features, FEAT_PRICE));
setPlanPeriodMonths(getInt(features, FEAT_PERIOD_MONTHS));
setPlanTrial(getBoolean(features, FEAT_IS_TRIAL));
// Read features we understand
setPtbEnabled(getBoolean(features, FEAT_PTB_ENABLED, DEFAULT_PTB_ENABLED));
setPtbAdvanced(getBoolean(features, FEAT_PTB_ADVANCED, DEFAULT_PTB_ADVANCED));
setPublicationLimit(getInt(features, FEAT_PUB_LIMIT, DEFAULT_PUB_LIMIT));
setSubscriptionLimit(getInt(features, FEAT_SUB_LIMIT, DEFAULT_SUB_LIMIT));
setSynchronizationLimit(getInt(features, FEAT_SYN_LIMIT, DEFAULT_SYN_LIMIT));
setSfDeduplication(getBoolean(features, FEAT_SF_DEDUPLICATION, DEFAULT_SF_DEDUPLICATION));
setAutoSaving(getBoolean(features, FEAT_AUTO_SAVING, DEFAULT_AUTO_SAVING));
setSentimentsEnabled(getBoolean(features, FEAT_SA_ENABLED, DEFAULT_SENTIMENTS_ENABLED));
}
/**
* Checks if the given features collection is valid before using it.
*
* @param features features.
*
* @return <code>TRUE</code> if valid and ready for consumption.
*/
private static boolean isValid(Map<String, String> features)
{
return StringUtils.isNotEmpty(features.get(FEAT_NAME)) &&
isDate(features, FEAT_EXP_DATE) &&
isFloat(features, FEAT_PRICE) &&
isInt(features, FEAT_PERIOD_MONTHS, false) &&
isBoolean(features, FEAT_IS_TRIAL);
}
/**
* Checks if the given feature exists and is a valid integer.
*
* @param features features collection.
* @param name feature name.
* @param unlimIsPossible <code>TRUE</code> if unlimited value ('-') is possible.
*
* @return <code>TRUE</code> if a valid integer value.
*/
static boolean isInt(Map<String, String> features, String name, boolean unlimIsPossible)
{
String v = features.get(name);
return StringUtils.isNotEmpty(v) && (v.matches("[0-9]+") || (unlimIsPossible && "-".equals(v)));
}
/**
* Checks if the given feature exists and is a valid date.
*
* @param features features collection.
* @param name feature name.
*
* @return <code>TRUE</code> if a valid date value.
*/
static boolean isDate(Map<String, String> features, String name)
{
return isInt(features, name, false);
}
/**
* Checks if the given feature exists and is a valid boolean.
*
* @param features features collection.
* @param name feature name.
*
* @return <code>TRUE</code> if a valid boolean value.
*/
static boolean isBoolean(Map<String, String> features, String name)
{
String v = features.get(name);
return StringUtils.isNotEmpty(v) && v.matches("(0|1)");
}
/**
* Checks if the given feature exists and is a valid float.
*
* @param features features collection.
* @param name feature name.
*
* @return <code>TRUE</code> if a valid float value.
*/
static boolean isFloat(Map<String, String> features, String name)
{
String v = features.get(name);
return StringUtils.isNotEmpty(v) && v.matches("[0-9]+(\\.[0-9]+)?");
}
// ------------------------------------------------------------------------
// Storage procedures
// ------------------------------------------------------------------------
/**
* Loads features from the preferences.
*/
private void loadFeatures()
{
if (appPrefs == null)
{
revertToDefault();
return;
}
String f = decode(appPrefs.get(PREFS_KEY, null));
if (StringUtils.isNotEmpty(f))
{
Map<String, String> features = new HashMap<String, String>();
String[] featurePairs = f.split("~");
for (String pair : featurePairs)
{
String[] p = pair.split("\\|");
features.put(p[0], p[1]);
}
if (isValid(features) && StringUtils.isNotEmpty(features.get(FEAT_HASH)))
{
parseNewFeatures(features);
planHash = features.get(FEAT_HASH);
} else
{
LOG.severe("Loaded features have incorrect format: " + f);
}
}
}
/**
* Saves features to the preferences.
*
* @param features features collection.
*/
private void storeFeatures(Map<String, String> features)
{
if (appPrefs == null) return;
List<String> featureList = new ArrayList<String>();
Set<Map.Entry<String,String>> fs = features.entrySet();
for (Map.Entry<String, String> feature : fs)
{
featureList.add(feature.getKey() + '|' + feature.getValue());
}
appPrefs.put(PREFS_KEY, encode(StringUtils.join(featureList.iterator(), "~")));
}
/**
* Simple encoder.
*
* @param s string to encode.
*
* @return encoded version.
*/
static String encode(String s)
{
String v = s;
if (StringUtils.isNotEmpty(s))
{
byte[] bs = s.getBytes();
StringBuffer sb = new StringBuffer(bs.length);
for (byte b : bs)
{
if (b < 16) sb.append(0);
sb.append(Integer.toHexString(b));
}
v = sb.toString();
}
return v;
}
/**
* Simple decoder.
*
* @param s string to decode.
*
* @return decoded version.
*/
static String decode(String s)
{
String v = s;
if (StringUtils.isNotEmpty(s))
{
byte[] bb = new byte[s.length() / 2];
for (int i = 0; i < s.length(); i += 2)
{
String b = "" + s.charAt(i) + s.charAt(i+1);
bb[i / 2] = (byte)Integer.parseInt(b, 16);
}
v = new String(bb);
}
return v;
}
// ------------------------------------------------------------------------
// Helper functions to parse features
// ------------------------------------------------------------------------
private Date getDate(Map<String, String> features, String name)
{
long date = Long.parseLong(features.get(name));
return date == 0 ? null : new Date(date);
}
private static int getInt(Map<String, String> features, String name)
{
return getInt(features, name, Integer.MIN_VALUE);
}
private static int getInt(Map<String, String> features, String name, int def)
{
String v = features.get(name);
return v == null ? def : "-".equals(v) ? -1 : Integer.parseInt(v);
}
private static boolean getBoolean(Map<String, String> features, String name)
{
return getBoolean(features, name, false);
}
private static boolean getBoolean(Map<String, String> features, String name, boolean def)
{
int v = getInt(features, name);
return v == Integer.MIN_VALUE ? def : v == 1;
}
private static float getFloat(Map<String, String> features, String name)
{
return Float.parseFloat(features.get(name));
}
// ------------------------------------------------------------------------
// Property change support
// ------------------------------------------------------------------------
/**
* Adds property change listener.
*
* @param l pcl.
*/
public void addPropertyChangeListener(PropertyChangeListener l)
{
pcs.addPropertyChangeListener(l);
}
/**
* Removes property change listener.
*
* @param l pcl.
*/
public void removePropertyChangeListener(PropertyChangeListener l)
{
pcs.removePropertyChangeListener(l);
}
/**
* Adds property change listener.
*
* @param prop property name.
* @param l pcl.
*/
public void addPropertyChangeListener(String prop, PropertyChangeListener l)
{
pcs.addPropertyChangeListener(prop, l);
}
/**
* Removes property change listener.
*
* @param prop property name.
* @param l pcl.
*/
public void removePropertyChangeListener(String prop, PropertyChangeListener l)
{
pcs.removePropertyChangeListener(prop, l);
}
// ------------------------------------------------------------------------
// Plan properties
// ------------------------------------------------------------------------
/**
* Returns the name of the current plan.
*
* @return name.
*/
public String getPlanName()
{
return planName;
}
/**
* Sets the name of the current plan.
*
* @param name name.
*/
public void setPlanName(String name)
{
this.planName = name;
}
/**
* Returns plan hash.
*
* @return hash.
*/
public String getPlanHash()
{
return planHash;
}
/**
* Sets plan hash.
*
* @param planHash hash.
*/
void setPlanHash(String planHash)
{
this.planHash = planHash;
}
/**
* Returns plan expiration date.
*
* @return exp date.
*/
public Date getPlanExpirationDate()
{
return planExpirationDate;
}
/**
* Sets plan expiration date.
*
* @param date date.
*/
void setPlanExpirationDate(Date date)
{
planExpirationDate = date;
}
/**
* Returns plan period in months.
*
* @return months.
*/
public int getPlanPeriodMonths()
{
return planPeriodMonths;
}
/**
* Sets plan period in months.
*
* @param period period.
*/
void setPlanPeriodMonths(int period)
{
this.planPeriodMonths = period;
}
/**
* Returns plan price.
*
* @return price.
*/
public float getPlanPrice()
{
return planPrice;
}
/**
* Returns TRUE if the plain is the paid one.
*
* @return TRUE if the plain is the paid one.
*/
public boolean isPaidPlan()
{
return planName != null && !"free".equalsIgnoreCase(planName);
}
/**
* Sets plan price.
*
* @param price price.
*/
void setPlanPrice(float price)
{
this.planPrice = price;
}
/**
* Returns <code>TRUE</code> if plan is trial.
*
* @return <code>TRUE</code> if plan is trial.
*/
public boolean isPlanTrial()
{
return planTrial;
}
/**
* Sets the trial plan flag.
*
* @param trial trial.
*/
void setPlanTrial(boolean trial)
{
this.planTrial = trial;
}
// ------------------------------------------------------------------------
// Features
// ------------------------------------------------------------------------
/**
* Returns the number of lists allowed for publishing.
*
* @return the limit or '-1' for unlimited.
*/
public int getPublicationLimit()
{
return publicationLimit;
}
/**
* Sets new published list limit.
*
* @param limit limit.
*/
private void setPublicationLimit(int limit)
{
int old = publicationLimit;
publicationLimit = limit;
pcs.firePropertyChange(PROP_PUBLICATION_LIMIT, old, limit);
}
/**
* Returns the maximum number of subscriptions a user can have.
*
* @return the limit or '-1' for unlimited.
*/
public int getSubscriptionLimit()
{
return subscriptionLimit;
}
/**
* Sets new subscriptions limit.
*
* @param limit limit.
*/
private void setSubscriptionLimit(int limit)
{
int old = subscriptionLimit;
subscriptionLimit = limit;
pcs.firePropertyChange(PROP_SUBSCRIPTION_LIMIT, old, limit);
}
/**
* Returns the maximum number of syncs a user can have during the day.
*
* @return the limit.
*/
public int getSynchronizationLimit()
{
return synchronizationLimit;
}
/**
* Sets new sync limit.
*
* @param limit limit.
*/
void setSynchronizationLimit(int limit)
{
int old = synchronizationLimit;
synchronizationLimit = limit;
pcs.firePropertyChange(PROP_SYNCHRONIZATION_LIMIT, old, limit);
}
/**
* Returns <code>TRUE</code> when post-to-blog feature is enabled.
*
* @return <code>TRUE</code> when post-to-blog feature is enabled.
*/
public boolean isPtbEnabled()
{
return ptbEnabled;
}
/**
* Sets new PTB enabled flag.
*
* @param enabled flag.
*/
private void setPtbEnabled(boolean enabled)
{
boolean old = ptbEnabled;
ptbEnabled = enabled;
pcs.firePropertyChange(PROP_PTB_ENABLED, old, enabled);
}
/**
* Returns <code>TRUE</code> if the post-to-blog feature should be advanced
* (multiple blogs / advanced dialog).
*
* @return <code>TRUE</code> if the post-to-blog feature is advanced.
*/
public boolean isPtbAdvanced()
{
return ptbAdvanced;
}
/**
* Sets new PTB advanced flag.
*
* @param flag flag.
*/
private void setPtbAdvanced(boolean flag)
{
boolean old = ptbAdvanced;
ptbAdvanced = flag;
pcs.firePropertyChange(PROP_PTB_ADVANCED, old, flag);
}
/**
* Returns <code>TRUE</code> for enabled deduplication mode.
*
* @return <code>TRUE</code> if enabled.
*/
public boolean isSfDeduplication()
{
return sfDeduplication;
}
/**
* Sets the state of deduplication mode.
*
* @param en <code>TRUE</code> to enable.
*/
private void setSfDeduplication(boolean en)
{
boolean old = sfDeduplication;
this.sfDeduplication = en;
pcs.firePropertyChange(PROP_SF_DEDUPLICATION, old, en);
}
/**
* Returns <code>TRUE</code> if auto-saving is on.
*
* @return <code>TRUE</code> if auto-saving is on.
*/
public boolean isAutoSaving()
{
return autoSaving;
}
/**
* Enabled / disables auto-saving.
*
* @param en <code>TRUE</code> to enable.
*/
private void setAutoSaving(boolean en)
{
boolean old = autoSaving;
autoSaving = en;
pcs.firePropertyChange(PROP_AUTO_SAVING, old, en);
}
/**
* Returns TRUE if sentiments feature are enabled.
*
* @return TRUE if sentiments feature are enabled.
*/
public boolean isSentimentsEnabled()
{
return sentimentsEnabled;
}
/**
* Enables / disables sentiments feature.
*
* @param en TRUE to enable.
*/
private void setSentimentsEnabled(boolean en)
{
boolean old = sentimentsEnabled;
sentimentsEnabled = en;
pcs.firePropertyChange(PROP_SENTIMENTS_ENABLED, old, en);
}
/**
* Returns the task updating the plan features.
*
* @return task.
*/
public Runnable getUpdater()
{
return this;
}
/**
* Registers synchronization attempt.
*/
public void registerSync()
{
synchronized (SYNCS_LOCK)
{
getSynchronizationsCount(true);
}
}
/**
* Returns the number of synchronizations today.
* NOTE: Must be called with SYNCS_LOCK.
*
* @param inc <code>TRUE</code> to increment counter.
*
* @return syncs.
*/
int getSynchronizationsCount(boolean inc)
{
long today = DateUtils.getTodayTime();
long lastSyncDate = appPrefs.getLong(KEY_LAST_SYNC_DATE, -1);
int syncs;
// Get current syncs count and reset if the day has ended
boolean updated = false;
if (lastSyncDate < today)
{
updated = true;
syncs = 0;
} else syncs = appPrefs.getInt(KEY_SYNCHRONIZATIONS, 0);
// Increment if required
if (inc) syncs++;
// Save if updated
if (updated || inc)
{
appPrefs.putLong(KEY_LAST_SYNC_DATE, System.currentTimeMillis());
appPrefs.putInt(KEY_SYNCHRONIZATIONS, syncs);
}
return syncs;
}
/**
* Checks if the user can synchronize today.
*
* @return <code>TRUE</code> if the user can synchronize.
*/
public boolean canSynchronize()
{
boolean res = true;
if (synchronizationLimit > -1)
{
synchronized (SYNCS_LOCK)
{
res = getSynchronizationsCount(false) < synchronizationLimit;
}
}
return res;
}
/**
* Returns TRUE if the user is registered.
*
* NOTE: This checks only if the user has entered their email / password.
*
* @return TRUE if registered.
*/
public boolean isRegistered()
{
String email = servicePrefs.getEmail();
String password = servicePrefs.getPassword();
return StringUtils.isNotEmpty(email) && StringUtils.isNotEmpty(password);
}
}