/* This code is part of WoT, a plugin for Freenet. It is distributed
* under the GNU General Public License, version 2 (or at your option
* any later version). See http://www.gnu.org/ for details of the GPL. */
package plugins.WoT;
import java.net.MalformedURLException;
import java.util.Date;
import java.util.HashSet;
import plugins.WoT.exceptions.DuplicateIdentityException;
import plugins.WoT.exceptions.DuplicateScoreException;
import plugins.WoT.exceptions.DuplicateTrustException;
import plugins.WoT.exceptions.InvalidParameterException;
import plugins.WoT.exceptions.NotInTrustTreeException;
import plugins.WoT.exceptions.NotTrustedException;
import plugins.WoT.exceptions.UnknownIdentityException;
import plugins.WoT.introduction.IntroductionClient;
import plugins.WoT.introduction.IntroductionPuzzle;
import plugins.WoT.introduction.IntroductionPuzzleStore;
import plugins.WoT.introduction.IntroductionServer;
import plugins.WoT.ui.fcp.FCPInterface;
import plugins.WoT.ui.web.WebInterface;
import com.db4o.Db4o;
import com.db4o.ObjectContainer;
import com.db4o.ObjectSet;
import com.db4o.config.Configuration;
import com.db4o.ext.ExtObjectContainer;
import com.db4o.query.Query;
import com.db4o.reflect.jdk.JdkReflector;
import freenet.keys.FreenetURI;
import freenet.l10n.L10n.LANGUAGE;
import freenet.node.RequestClient;
import freenet.pluginmanager.FredPlugin;
import freenet.pluginmanager.FredPluginFCP;
import freenet.pluginmanager.FredPluginHTTP;
import freenet.pluginmanager.FredPluginL10n;
import freenet.pluginmanager.FredPluginRealVersioned;
import freenet.pluginmanager.FredPluginThreadless;
import freenet.pluginmanager.FredPluginVersioned;
import freenet.pluginmanager.FredPluginWithClassLoader;
import freenet.pluginmanager.PluginHTTPException;
import freenet.pluginmanager.PluginReplySender;
import freenet.pluginmanager.PluginRespirator;
import freenet.support.Logger;
import freenet.support.SimpleFieldSet;
import freenet.support.api.Bucket;
import freenet.support.api.HTTPRequest;
/**
* A web of trust plugin based on Freenet.
*
* @author xor (xor@freenetproject.org), Julien Cornuwel (batosai@freenetproject.org)
*/
public class WoT implements FredPlugin, FredPluginHTTP, FredPluginThreadless, FredPluginFCP, FredPluginVersioned, FredPluginRealVersioned,
FredPluginL10n, FredPluginWithClassLoader {
/* Constants */
public static final String DATABASE_FILENAME = "WebOfTrust-testing.db4o";
public static final int DATABASE_FORMAT_VERSION = -99;
/** The relative path of the plugin on Freenet's web interface */
public static final String SELF_URI = "/plugins/plugins.WoT.WoT";
/**
* The "name" of this web of trust. It is included in the document name of identity URIs. For an example, see the SEED_IDENTITIES
* constant below. The purpose of this costant is to allow anyone to create his own custom web of trust which is completely disconnected
* from the "official" web of trust of the Freenet project.
*/
public static final String WOT_NAME = "WoT-testing";
/**
* The official seed identities of the WoT plugin: If a newbie wants to download the whole offficial web of trust, he needs at least one
* trust list from an identity which is well-connected to the web of trust. To prevent newbies from having to add this identity manually,
* the Freenet development team provides a list of seed identities - each of them is one of the developers.
*/
private static final String[] SEED_IDENTITIES = new String[] {
"USK@fWK9InP~vG6HnTDm3wiJgvh6ULJQaU5XYTkXXNuKTTk,GnZgrilXSYjD~xrD6l4~5x~Nspz3aFe2eYXWRvaNRHU,AQACAAE/WoT/0" // xor
/* FIXME: Add the developers. But first we need to debug :) */
};
/* References from the node */
/** The ClassLoader which was used to load the plugin JAR, needed by db4o to work correctly */
private ClassLoader mClassLoader;
/** The node's interface to connect the plugin with the node, needed for retrieval of all other interfaces */
private PluginRespirator mPR;
/* References from the plugin itself */
/* Database & configuration of the plugin */
private ExtObjectContainer mDB;
private Config mConfig;
private IntroductionPuzzleStore mPuzzleStore;
/** Used for exporting identities, identity introductions and introduction puzzles to XML and importing them from XML. */
private XMLTransformer mXMLTransformer;
private RequestClient mRequestClient;
/* Worker objects which actually run the plugin */
/**
* Periodically wakes up and inserts any OwnIdentity which needs to be inserted.
*/
private IdentityInserter mInserter;
/**
* Fetches identities when it is told to do so by the plugin:
* - At startup, all known identities are fetched
* - When a new identity is received from a trust list it is fetched
* - When a new identity is received by the IntrouductionServer it is fetched
* - When an identity is manually added it is also fetched.
* - ...
*/
private IdentityFetcher mFetcher;
/**
* Uploads captchas belonging to our own identities which others can solve to get on the trust list of them. Checks whether someone
* uploaded solutions for them periodically and adds the new identities if a solution is received.
*/
private IntroductionServer mIntroductionServer;
/**
* Downloads captchas which the user can solve to announce his identities on other people's trust lists, provides the interface for
* the UI to obtain the captchas and enter solutions. Uploads the solutions if the UI enters them.
*/
private IntroductionClient mIntroductionClient;
/* User interfaces */
private WebInterface mWebInterface;
private FCPInterface mFCPInterface;
public void runPlugin(PluginRespirator myPR) {
try {
Logger.debug(this, "Start");
/* Catpcha generation needs headless mode on linux */
System.setProperty("java.awt.headless", "true");
mPR = myPR;
mDB = initDB(DATABASE_FILENAME); /* FIXME: Change before release */
mConfig = Config.loadOrCreate(this);
if(mConfig.getInt(Config.DATABASE_FORMAT_VERSION) > WoT.DATABASE_FORMAT_VERSION)
throw new RuntimeException("The WoT plugin's database format is newer than the WoT plugin which is being used.");
upgradeDB();
deleteDuplicateObjects();
deleteOrphanObjects();
mXMLTransformer = new XMLTransformer(this);
mPuzzleStore = new IntroductionPuzzleStore(this);
mRequestClient = new RequestClient() {
public boolean persistent() {
return false;
}
public void removeFrom(ObjectContainer container) {
throw new UnsupportedOperationException();
}
};
createSeedIdentities();
mInserter = new IdentityInserter(this);
mFetcher = new IdentityFetcher(this);
mIntroductionServer = new IntroductionServer(this, mFetcher);
mIntroductionClient = new IntroductionClient(this);
mWebInterface = new WebInterface(this, SELF_URI);
mFCPInterface = new FCPInterface(this);
Logger.debug(this, "Starting fetches of all identities...");
synchronized(this) {
for(Identity identity : getAllIdentities())
mFetcher.fetch(identity, true);
}
Logger.debug(this, "WoT startup completed.");
}
catch(Exception e) {
Logger.error(this, "Error during startup", e);
/* We call it so the database is properly closed */
terminate();
}
}
public WoT() {
}
/**
* For use by the unit tests to be able to run WoT without a node.
* @param databaseFilename The filename of the database.
*/
public WoT(String databaseFilename) {
mDB = initDB(databaseFilename);
mConfig = Config.loadOrCreate(this);
if(mConfig.getInt(Config.DATABASE_FORMAT_VERSION) > WoT.DATABASE_FORMAT_VERSION)
throw new RuntimeException("The WoT plugin's database format is newer than the WoT plugin which is being used.");
}
/**
* Initializes the plugin's db4o database.
*
* @return A db4o ObjectContainer.
*/
private ExtObjectContainer initDB(String filename) {
Configuration cfg = Db4o.newConfiguration();
cfg.reflectWith(new JdkReflector(mClassLoader));
cfg.activationDepth(5); /* FIXME: Change to 1 and add explicit activation everywhere */
cfg.exceptionsOnNotStorable(true);
for(String field : Identity.getIndexedFields()) cfg.objectClass(Identity.class).objectField(field).indexed(true);
for(String field : OwnIdentity.getIndexedFields()) cfg.objectClass(OwnIdentity.class).objectField(field).indexed(true);
for(String field : Trust.getIndexedFields()) cfg.objectClass(Trust.class).objectField(field).indexed(true);
for(String field : Score.getIndexedFields()) cfg.objectClass(Score.class).objectField(field).indexed(true);
cfg.objectClass(IntroductionPuzzle.PuzzleType.class).persistStaticFieldValues(); /* Needed to be able to store enums */
for(String field : IntroductionPuzzle.getIndexedFields()) cfg.objectClass(IntroductionPuzzle.class).objectField(field).indexed(true);
return Db4o.openFile(cfg, filename).ext();
}
private synchronized void upgradeDB() {
int oldVersion = mConfig.getInt(Config.DATABASE_FORMAT_VERSION);
if(oldVersion == WoT.DATABASE_FORMAT_VERSION)
return;
if(oldVersion == -100) {
Logger.normal(this, "Found old database (-100), adding last fetched date to all identities ...");
for(Identity identity : getAllIdentities()) {
identity.mLastFetchedDate = new Date(0);
storeAndCommit(identity);
}
mConfig.set(Config.DATABASE_FORMAT_VERSION, WoT.DATABASE_FORMAT_VERSION);
mConfig.storeAndCommit();
}
else
throw new RuntimeException("Your database is too outdated to be upgraded automatically, please create a new one by deleting "
+ DATABASE_FILENAME + ". Contact the developers if you really need your old data.");
}
/**
* Debug function for deleting duplicate identities etc. which might have been created due to bugs :)
*/
@SuppressWarnings("unchecked")
private synchronized void deleteDuplicateObjects() {
synchronized(mDB) {
try {
ObjectSet identities = getAllIdentities();
HashSet deleted = new HashSet();
for(Identity identity : identities) {
Query q = mDB.query();
q.constrain(Identity.class);
q.descend("mID").constrain(identity.getID());
q.constrain(identity).identity().not();
ObjectSet duplicates = q.execute();
for(Identity duplicate : duplicates) {
if(deleted.contains(duplicate.getID()) == false) {
Logger.error(duplicate, "Deleting duplicate identity " + duplicate.getRequestURI());
for(Trust trust : getReceivedTrusts(duplicate))
mDB.delete(trust);
for(Trust trust : getGivenTrusts(duplicate))
mDB.delete(trust);
for(Score score : getScores(duplicate))
mDB.delete(score);
mDB.delete(duplicate);
}
}
deleted.add(identity.getID());
mDB.commit();
}
}
catch(RuntimeException e) {
mDB.rollback();
Logger.error(this, "Error while deleting duplicate identities", e);
}
try {
for(OwnIdentity treeOwner : getAllOwnIdentities()) {
HashSet givenTo = new HashSet();
for(Trust trust : getGivenTrusts(treeOwner)) {
if(givenTo.contains(trust.getTrustee().getID()) == false)
givenTo.add(trust.getTrustee().getID());
else {
Identity trustee = trust.getTrustee();
Logger.error(this, "Deleting duplicate given trust from " + treeOwner.getNickname() + " to " +
trustee.getNickname());
mDB.delete(trust);
try {
updateScoreWithoutCommit(treeOwner, trustee);
}
catch(Exception e) { /* Maybe another duplicate prevents it from working ... */
Logger.error(this, "Updating score of " + trustee.getNickname() + " failed.", e);
}
}
}
mDB.commit();
}
}
catch(RuntimeException e) {
mDB.rollback();
Logger.error(this, "Error while deleting duplicate trusts", e);
}
}
/* FIXME: Also delete duplicate trust, score, etc. */
}
/**
* Debug function for deleting trusts or scores of which one of the involved partners is missing.
*/
@SuppressWarnings("unchecked")
private synchronized void deleteOrphanObjects() {
synchronized(mDB) {
try {
Query q = mDB.query();
q.constrain(Trust.class);
q.descend("mTruster").constrain(null).identity().or(q.descend("mTrustee").constrain(null).identity());
ObjectSet orphanTrusts = q.execute();
for(Trust trust : orphanTrusts) {
Logger.error(trust, "Deleting orphan trust, truster = " + trust.getTruster() + ", trustee = " + trust.getTrustee());
mDB.delete(trust);
}
}
catch(Exception e) {
Logger.error(this, "Deleting orphan trusts failed.", e);
// mDB.rollback(); /* No need to do so here */
}
try {
Query q = mDB.query();
q.constrain(Score.class);
q.descend("mTreeOwner").constrain(null).identity().or(q.descend("mTarget").constrain(null).identity());
ObjectSet orphanScores = q.execute();
for(Score score : orphanScores) {
Logger.error(score, "Deleting orphan score, treeOwner = " + score.getTreeOwner() + ", target = " + score.getTarget());
mDB.delete(score);
}
mDB.commit();
}
catch(Exception e) {
Logger.error(this, "Deleting orphan trusts failed.", e);
// mDB.rollback(); /* No need to do so here */
}
}
}
private synchronized void createSeedIdentities() {
for(String seedURI : SEED_IDENTITIES) {
Identity seed;
try {
seed = getIdentityByURI(seedURI);
if(seed instanceof OwnIdentity) {
OwnIdentity ownSeed = (OwnIdentity)seed;
// FIXME: Does the cast make that necessary? I'm adding it to make sure that we do not lose information when storing
mDB.activate(ownSeed, 5);
ownSeed.addContext(IntroductionPuzzle.INTRODUCTION_CONTEXT);
ownSeed.setProperty(IntroductionServer.PUZZLE_COUNT_PROPERTY,
Integer.toString(IntroductionServer.SEED_IDENTITY_PUZZLE_COUNT));
storeAndCommit(ownSeed);
}
try {
seed.setEdition(new FreenetURI(seedURI).getEdition());
} catch(Exception e) {
/* We already have the latest edition stored */
}
} catch (UnknownIdentityException uie) {
try {
seed = new Identity(seedURI, null, true);
storeAndCommit(seed);
} catch (Exception e) {
Logger.error(this, "Seed identity creation error", e);
}
} catch (Exception e) {
Logger.error(this, "Seed identity loading error", e);
}
}
}
public void terminate() {
Logger.debug(this, "WoT plugin terminating ...");
/* We use single try/catch blocks so that failure of termination of one service does not prevent termination of the others */
try {
if(mIntroductionClient != null)
mIntroductionClient.terminate();
}
catch(Exception e) {
Logger.error(this, "Error during termination.", e);
}
try {
if(mIntroductionServer != null)
mIntroductionServer.terminate();
}
catch(Exception e) {
Logger.error(this, "Error during termination.", e);
}
try {
if(mInserter != null)
mInserter.terminate();
}
catch(Exception e) {
Logger.error(this, "Error during termination.", e);
}
try {
if(mFetcher != null)
mFetcher.stop();
}
catch(Exception e) {
Logger.error(this, "Error during termination.", e);
}
try {
if(mDB != null) {
/* FIXME: Is it possible to ask db4o whether a transaction is pending? If the plugin's synchronization works correctly,
* NONE should be pending here and we should log an error if there are any pending transactions at this point. */
synchronized(mDB.lock()) {
mDB.rollback();
mDB.close();
}
}
}
catch(Exception e) {
Logger.error(this, "Error during termination.", e);
}
Logger.debug(this, "WoT plugin terminated.");
}
/**
* Inherited event handler from FredPluginHTTP, handled in class WebInterface.
*/
public String handleHTTPGet(HTTPRequest request) throws PluginHTTPException {
return mWebInterface.handleHTTPGet(request);
}
/**
* Inherited event handler from FredPluginHTTP, handled in class WebInterface.
*/
public String handleHTTPPost(HTTPRequest request) throws PluginHTTPException {
return mWebInterface.handleHTTPPost(request);
}
/**
* Inherited event handler from FredPluginHTTP, handled in class WebInterface.
*/
public String handleHTTPPut(HTTPRequest request) throws PluginHTTPException {
return mWebInterface.handleHTTPPut(request);
}
/**
* Inherited event handler from FredPluginFCP, handled in class FCPInterface.
*/
public void handle(PluginReplySender replysender, SimpleFieldSet params, Bucket data, int accesstype) {
mFCPInterface.handle(replysender, params, data, accesstype);
}
/**
* Loads an own or normal identity from the database, querying on its ID.
*
* @param id The ID of the identity to load
* @return The identity matching the supplied ID.
* @throws DuplicateIdentityException if there are more than one identity with this id in the database
* @throws UnknownIdentityException if there is no identity with this id in the database
*/
@SuppressWarnings("unchecked")
public synchronized Identity getIdentityByID(String id) throws UnknownIdentityException {
Query query = mDB.query();
query.constrain(Identity.class);
query.descend("mID").constrain(id);
ObjectSet result = query.execute();
if(result.size() == 0)
throw new UnknownIdentityException(id);
if(result.size() > 1)
throw new DuplicateIdentityException(id);
return result.next();
}
/**
* Gets an OwnIdentity by its ID.
*
* @param id The unique identifier to query an OwnIdentity
* @return The requested OwnIdentity
* @throws UnknownIdentityException if there is now OwnIdentity with that id
*/
@SuppressWarnings("unchecked")
public synchronized OwnIdentity getOwnIdentityByID(String id) throws UnknownIdentityException {
Query query = mDB.query();
query.constrain(OwnIdentity.class);
query.descend("mID").constrain(id);
ObjectSet result = query.execute();
if(result.size() == 0)
throw new UnknownIdentityException(id);
if(result.size() > 1)
throw new DuplicateIdentityException(id);
return result.next();
}
/**
* Loads an identity from the database, querying on its requestURI (a valid {@link FreenetURI})
*
* @param uri The requestURI of the identity
* @return The identity matching the supplied requestURI
* @throws UnknownIdentityException if there is no identity with this id in the database
* @throws DuplicateIdentityException if there are more than one identity with this id in the database
*/
public Identity getIdentityByURI(FreenetURI uri) throws UnknownIdentityException {
return getIdentityByID(Identity.getIDFromURI(uri));
}
/**
* Loads an identity from the database, querying on its requestURI (as String)
*
* @param uri The requestURI of the identity which will be converted to {@link FreenetURI}
* @return The identity matching the supplied requestURI
* @throws UnknownIdentityException if there is no identity with this id in the database
* @throws DuplicateIdentityException if there are more than one identity with this id in the database
* @throws MalformedURLException if the requestURI isn't a valid FreenetURI
*/
public Identity getIdentityByURI(String uri) throws UnknownIdentityException, MalformedURLException {
return getIdentityByURI(new FreenetURI(uri));
}
/**
* Gets an OwnIdentity by its requestURI (a {@link FreenetURI}).
* The OwnIdentity's unique identifier is extracted from the supplied requestURI.
*
* @param uri The requestURI of the desired OwnIdentity
* @return The requested OwnIdentity
* @throws UnknownIdentityException if the OwnIdentity isn't in the database
*/
public OwnIdentity getOwnIdentityByURI(FreenetURI uri) throws UnknownIdentityException {
return getOwnIdentityByID(OwnIdentity.getIDFromURI(uri));
}
/**
* Gets an OwnIdentity by its requestURI (as String).
* The given String is converted to {@link FreenetURI} in order to extract a unique id.
*
* @param db A reference to the database
* @param uri The requestURI (as String) of the desired OwnIdentity
* @return The requested OwnIdentity
* @throws UnknownIdentityException if the OwnIdentity isn't in the database
* @throws DuplicateIdentityException if the OwnIdentity is present more that once in the database (should never happen)
* @throws MalformedURLException if the supplied requestURI is not a valid FreenetURI
*/
public OwnIdentity getOwnIdentityByURI(String uri) throws UnknownIdentityException, MalformedURLException {
return getOwnIdentityByURI(new FreenetURI(uri));
}
/**
* Returns all identities that are in the database
* You have to synchronize on this WoT when calling the function and processing the returned list!
*
* @return An {@link ObjectSet} containing all identities present in the database
*/
public synchronized ObjectSet getAllIdentities() {
return mDB.queryByExample(Identity.class);
}
/**
* Returns all non-own identities that are in the database.
*
* You have to synchronize on this WoT when calling the function and processing the returned list!
*/
@SuppressWarnings("unchecked")
public synchronized ObjectSet getAllNonOwnIdentities() {
Query q = mDB.query();
q.constrain(Identity.class);
q.constrain(OwnIdentity.class).not();
return q.execute();
}
/**
* Returns all non-own identities that are in the database, sorted descending by their date of modification, i.e. recently
* modified identities will be at the beginning of the list.
*
* You have to synchronize on this WoT when calling the function and processing the returned list!
*
* Used by the IntroductionClient for fetching puzzles from recently modified identities.
*/
@SuppressWarnings("unchecked")
public synchronized ObjectSet getAllNonOwnIdentitiesSortedByModification () {
Query q = mDB.query();
q.constrain(Identity.class);
q.constrain(OwnIdentity.class).not();
/* FIXME: As soon as identities announce that they were online every day, uncomment the following line */
/* q.descend("mLastChangedDate").constrain(new Date(CurrentTimeUTC.getInMillis() - 1 * 24 * 60 * 60 * 1000)).greater(); */
q.descend("mLastFetchedDate").orderDescending();
return q.execute();
}
/**
* Returns all own identities that are in the database
* You have to synchronize on this WoT when calling the function and processing the returned list!
*
* @return An {@link ObjectSet} containing all identities present in the database.
*/
public synchronized ObjectSet getAllOwnIdentities() {
return mDB.queryByExample(OwnIdentity.class);
}
public synchronized void storeAndCommit(Identity identity) {
synchronized(identity) {
synchronized(mDB.lock()) {
if(mDB.ext().isStored(identity) && !mDB.ext().isActive(identity))
throw new RuntimeException("Trying to store an inactive Identity object!");
/* FIXME: We also need to check whether the member objects are active here!!! */
try {
if(identity instanceof OwnIdentity) {
OwnIdentity ownId = (OwnIdentity)identity;
mDB.store(ownId.mInsertURI);
// mDB.store(ownId.mCreationDate); /* Not stored because db4o considers it as a primitive and automatically stores it. */
// mDB.store(ownId.mLastInsertDate); /* Not stored because db4o considers it as a primitive and automatically stores it. */
}
// mDB.store(mID); /* Not stored because db4o considers it as a primitive and automatically stores it. */
mDB.store(identity.mRequestURI);
// mDB.store(mFirstFetchedDate); /* Not stored because db4o considers it as a primitive and automatically stores it. */
// mDB.store(mLastFetchedDate); /* Not stored because db4o considers it as a primitive and automatically stores it. */
// mDB.store(mLastChangedDate); /* Not stored because db4o considers it as a primitive and automatically stores it. */
// mDB.store(mNickname); /* Not stored because db4o considers it as a primitive and automatically stores it. */
// mDB.store(mDoesPublishTrustList); /* Not stored because db4o considers it as a primitive and automatically stores it. */
mDB.store(identity.mProperties);
mDB.store(identity.mContexts);
mDB.store(identity);
mDB.commit();
Logger.debug(identity, "COMMITED.");
}
catch(RuntimeException e) {
mDB.rollback();
throw e;
}
}
}
}
/**
* Gets the score of this identity in a trust tree.
* Each {@link OwnIdentity} has its own trust tree.
*
* @param treeOwner The owner of the trust tree
* @param db A reference to the database
* @return The {@link Score} of this Identity in the required trust tree
* @throws NotInTrustTreeException if this identity is not in the required trust tree
*/
@SuppressWarnings("unchecked")
public synchronized Score getScore(OwnIdentity treeOwner, Identity target) throws NotInTrustTreeException {
Query query = mDB.query();
query.constrain(Score.class);
query.descend("mTreeOwner").constrain(treeOwner).identity();
query.descend("mTarget").constrain(target).identity();
ObjectSet result = query.execute();
if(result.size() == 0)
throw new NotInTrustTreeException(target.getRequestURI() + " is not in that trust tree");
if(result.size() > 1)
throw new DuplicateScoreException(target.getRequestURI() +" ("+ target.getNickname() +") has " + result.size() +
" scores in " + treeOwner.getNickname() +"'s trust tree");
return result.next();
}
/*
* FIXME:
* I suggest before releasing we should write a getRealScore() function which recalculates the score from all Trust objects which are
* stored in the database. We could then assert(getScore() == getRealScore()) for verifying that the database is consistent and watch
* for some time whether it stays consistent, just to make sure that there are no flaws in the code.
*/
/**
* Gets a list of all this Identity's Scores.
* You have to synchronize on this WoT around the call to this function and the processing of the returned list!
*
* @param db A reference to the database
* @return An {@link ObjectSet} containing all {@link Score} this Identity has.
*/
@SuppressWarnings("unchecked")
public synchronized ObjectSet getScores(Identity identity) {
Query query = mDB.query();
query.constrain(Score.class);
query.descend("mTarget").constrain(identity).identity();
return query.execute();
}
/**
* Gets the best score this Identity has in existing trust trees, 0 if it is not in the trust tree.
*
* @return the best score this Identity has
*/
public synchronized int getBestScore(Identity identity) {
int bestScore = 0;
ObjectSet scores = getScores(identity);
/* TODO: Use a db4o native query for this loop. Maybe use an index, indexes should be sorted so maximum search will be O(1)...
* but I guess indexes cannot be done for the (target, value) pair so we might just cache the best score ourselves...
* OTOH the number of own identies will be small so the maximum search will probably be fast... */
while(scores.hasNext()) {
Score score = scores.next();
bestScore = Math.max(score.getScore(), bestScore);
}
return bestScore;
}
/**
* Get all scores in the database.
* You have to synchronize on this WoT when calling the function and processing the returned list!
*/
public synchronized ObjectSet getAllScores() {
return mDB.queryByExample(Score.class);
}
/**
* Gets Identities matching a specified score criteria.
* You have to synchronize on this WoT when calling the function and processing the returned list!
*
* @param owner requestURI of the owner of the trust tree, null if you want the trusted identities of all owners.
* @param select Score criteria, can be '+', '0' or '-'
* @return an {@link ObjectSet} containing Identities that match the criteria
* @throws InvalidParameterException if the criteria is not recognised
*/
@SuppressWarnings("unchecked")
public synchronized ObjectSet getIdentitiesByScore(OwnIdentity treeOwner, int select) {
Query query = mDB.query();
query.constrain(Score.class);
if(treeOwner != null)
query.descend("mTreeOwner").constrain(treeOwner).identity();
query.descend("mTarget").constrain(OwnIdentity.class).not();
/* We include 0 in the list of identities with positive score because solving captchas gives no points to score */
if(select > 0)
query.descend("mValue").constrain(0).smaller().not();
else if(select < 0 )
query.descend("mValue").constrain(0).smaller();
else
query.descend("mValue").constrain(0);
return query.execute();
}
/**
* Gets {@link Trust} from a specified truster to a specified trustee.
*
* @param truster The identity that gives trust to this Identity
* @param trustee The identity which receives the trust
* @return The trust given to the trustee by the specified truster
* @throws NotTrustedException if the truster doesn't trust the trustee
*/
@SuppressWarnings("unchecked")
public synchronized Trust getTrust(Identity truster, Identity trustee) throws NotTrustedException, DuplicateTrustException {
Query query = mDB.query();
query.constrain(Trust.class);
query.descend("mTruster").constrain(truster).identity();
query.descend("mTrustee").constrain(trustee).identity();
ObjectSet result = query.execute();
if(result.size() == 0)
throw new NotTrustedException(truster.getNickname() + " does not trust " + trustee.getNickname());
if(result.size() > 1)
throw new DuplicateTrustException("Trust from " + truster.getNickname() + "to " + trustee.getNickname() + " exists "
+ result.size() + " times in the database");
return result.next();
}
/**
* Gets all trusts given by the given truster.
* You have to synchronize on this WoT when calling the function and processing the returned list!
*
* @return An {@link ObjectSet} containing all {@link Trust} the passed Identity has given.
*/
@SuppressWarnings("unchecked")
public synchronized ObjectSet getGivenTrusts(Identity truster) {
Query query = mDB.query();
query.constrain(Trust.class);
query.descend("mTruster").constrain(truster).identity();
return query.execute();
}
/**
* Gets all trusts given by the given truster in a trust list older than the given edition number.
* You have to synchronize on this WoT when calling the function and processing the returned list!
*/
@SuppressWarnings("unchecked")
protected synchronized ObjectSet getGivenTrustsOlderThan(Identity truster, long edition) {
Query q = mDB.query();
q.constrain(Trust.class);
q.descend("mTruster").constrain(truster).identity();
q.descend("mTrusterTrustListEdition").constrain(edition).smaller();
return q.execute();
}
/**
* Gets all trusts received by the given trustee.
* You have to synchronize on this WoT when calling the function and processing the returned list!
*
* @return An {@link ObjectSet} containing all {@link Trust} the passed Identity has received.
*/
@SuppressWarnings("unchecked")
public synchronized ObjectSet getReceivedTrusts(Identity trustee) {
Query query = mDB.query();
query.constrain(Trust.class);
query.descend("mTrustee").constrain(trustee).identity();
return query.execute();
}
/**
* Gets all trusts.
* You have to synchronize on this WoT when calling the function and processing the returned list!
*
* @return An {@link ObjectSet} containing all {@link Trust} the passed Identity has received.
*/
@SuppressWarnings("unchecked")
public synchronized ObjectSet getAllTrusts() {
Query query = mDB.query();
query.constrain(Trust.class);
return query.execute();
}
/**
* Gives some {@link Trust} to another Identity.
* It creates or updates an existing Trust object and make the trustee compute its {@link Score}.
*
* This function does neither lock the database nor commit the transaction. You have to surround it with
* synchronized(mDB.lock()) {
* try { ... setTrustWithoutCommit(...); storeAndCommit(truster); }
* catch(RuntimeException e) { mDB.rollback(); throw e; }
* }
*
* @param truster The Identity that gives the trust
* @param trustee The Identity that receives the trust
* @param newValue Numeric value of the trust
* @param newComment A comment to explain the given value
* @throws InvalidParameterException if a given parameter isn't valid, {@see Trust} for details on accepted values.
*/
protected synchronized void setTrustWithoutCommit(Identity truster, Identity trustee, byte newValue, String newComment)
throws InvalidParameterException {
Trust trust;
try { // Check if we are updating an existing trust value
trust = getTrust(truster, trustee);
trust.trusterEditionUpdated();
trust.setComment(newComment);
mDB.store(trust);
if(trust.getValue() != newValue) {
trust.setValue(newValue);
mDB.store(trust);
Logger.debug(this, "Updated trust value ("+ trust +"), now updating Score.");
updateScoreWithoutCommit(trustee);
}
} catch (NotTrustedException e) {
trust = new Trust(truster, trustee, newValue, newComment);
mDB.store(trust);
Logger.debug(this, "New trust value ("+ trust +"), now updating Score.");
updateScoreWithoutCommit(trustee);
}
truster.updated();
}
/**
* Only for being used by WoT internally and by unit tests!
*/
public synchronized void setTrust(Identity truster, Identity trustee, byte newValue, String newComment)
throws InvalidParameterException {
/* FIXME: Throw if we are no unit test and the truster is no own identity */
assert(truster instanceof OwnIdentity); /* Unit tests may ignore this. */
synchronized(mDB.lock()) {
try {
setTrustWithoutCommit(truster, trustee, newValue, newComment);
storeAndCommit(truster);
}
catch(RuntimeException e) {
mDB.rollback();
throw e;
}
}
}
/**
* Deletes a trust object
* @param truster
* @param trustee
*/
protected synchronized void removeTrust(OwnIdentity truster, Identity trustee) {
synchronized(mDB.lock()) {
try {
try {
Trust trust = getTrust(truster, trustee);
mDB.delete(trust);
updateScoreWithoutCommit(trustee);
} catch (NotTrustedException e) {
Logger.error(this, "Cannot remove trust - there is none - from " + truster.getNickname() + " to "
+ trustee.getNickname());
}
}
catch(RuntimeException e) {
mDB.rollback();
throw e;
}
}
}
/**
*
* This function does neither lock the database nor commit the transaction. You have to surround it with
* synchronized(mDB.lock()) {
* try { ... setTrustWithoutCommit(...); storeAndCommit(truster); }
* catch(RuntimeException e) { mDB.rollback(); throw e; }
* }
*
*/
protected synchronized void removeTrustWithoutCommit(Trust trust) {
mDB.delete(trust);
updateScoreWithoutCommit(trust.getTrustee());
}
/**
* Initializes this OwnIdentity's trust tree.
* Meaning : It creates a Score object for this OwnIdentity in its own trust tree,
* so it gets a rank and a capacity and can give trust to other Identities.
*
* @param db A reference to the database
* @throws DuplicateScoreException if there already is more than one Score for this identity (should never happen)
*/
private synchronized void initTrustTree(OwnIdentity identity) throws DuplicateScoreException {
synchronized(mDB.lock()) {
try {
getScore(identity, identity);
Logger.error(this, "initTrusTree called even though there is already one for " + identity);
return;
} catch (NotInTrustTreeException e) {
try {
mDB.store(new Score(identity, identity, 100, 0, 100));
mDB.commit();
}
catch(RuntimeException ex) {
mDB.rollback();
throw ex;
}
}
}
}
/**
* Updates this Identity's {@link Score} in every trust tree.
*
* This function does neither lock the database nor commit the transaction. You have to surround it with
* synchronized(mDB.lock()) {
* try { ... setTrustWithoutCommit(...); storeAndCommit(truster); }
* catch(RuntimeException e) { mDB.rollback(); throw e; }
* }
*
*/
private synchronized void updateScoreWithoutCommit(Identity trustee) {
ObjectSet treeOwners = getAllOwnIdentities();
if(treeOwners.size() == 0)
Logger.debug(this, "Can't update " + trustee.getNickname() + "'s score: there is no own identity yet");
while(treeOwners.hasNext())
updateScoreWithoutCommit(treeOwners.next(), trustee);
}
/**
* Updates this Identity's {@link Score} in one trust tree.
* Makes this Identity's trustees update their score if its capacity has changed.
*
* This function does neither lock the database nor commit the transaction. You have to surround it with
* synchronized(mDB.lock()) {
* try { ... setTrustWithoutCommit(...); storeAndCommit(truster); }
* catch(RuntimeException e) { mDB.rollback(); throw e; }
* }
*
* @param db A reference to the database
* @param treeOwner The OwnIdentity that owns the trust tree
*/
private synchronized void updateScoreWithoutCommit(OwnIdentity treeOwner, Identity target) {
if(target == treeOwner)
return;
boolean changedCapacity = false;
Logger.debug(target, "Updating " + target.getNickname() + "'s score in " + treeOwner.getNickname() + "'s trust tree...");
Score score;
int value = computeScoreValue(treeOwner, target);
int rank = computeRank(treeOwner, target);
if(rank == -1) { // -1 value means the identity is not in the trust tree
try { // If he had a score, we delete it
score = getScore(treeOwner, target);
mDB.delete(score); // He had a score, we delete it
changedCapacity = true;
Logger.debug(target, target.getNickname() + " is not in " + treeOwner.getNickname() + "'s trust tree anymore");
} catch (NotInTrustTreeException e) { }
}
else { // The identity is in the trust tree
/* We must detect if an identity had a null or negative score which has changed to positive:
* If we download the trust list of someone who has no or negative score we do not create the identities in his trust list.
* If the identity now gets a positive score we must re-download his current trust list. */
boolean scoreWasNegative = false;
try { // Get existing score or create one if needed
score = getScore(treeOwner, target);
scoreWasNegative = score.getScore() < 0;
} catch (NotInTrustTreeException e) {
score = new Score(treeOwner, target, 0, -1, 0);
scoreWasNegative = true;
}
if(scoreWasNegative && score.getScore() > 0) {
target.decreaseEdition();
Logger.debug(this, "Score changed from negative/null to positive, refetchting " + target.getRequestURI());
mFetcher.fetch(target);
}
score.setValue(value);
score.setRank(rank + 1);
int oldCapacity = score.getCapacity();
boolean hasNegativeTrust = false;
// Does the treeOwner personally distrust this identity ?
try {
if(getTrust(treeOwner, target).getValue() < 0) {
hasNegativeTrust = true;
Logger.debug(target, target.getNickname() + " received negative trust from " + treeOwner.getNickname() +
" and therefore has no capacity in his trust tree.");
}
} catch (NotTrustedException e) {}
if(hasNegativeTrust)
score.setCapacity(0);
else
score.setCapacity((score.getRank() >= Score.capacities.length) ? 1 : Score.capacities[score.getRank()]);
if(score.getCapacity() != oldCapacity)
changedCapacity = true;
mDB.store(score);
Logger.debug(target, "New score: " + score.toString());
}
if(changedCapacity) { // We have to update trustees' score
ObjectSet givenTrusts = getGivenTrusts(target);
Logger.debug(target, target.getNickname() + "'s capacity has changed in " + treeOwner.getNickname() +
"'s trust tree, updating his (" + givenTrusts.size() + ") trustees");
for(Trust givenTrust : givenTrusts)
updateScoreWithoutCommit(treeOwner, givenTrust.getTrustee());
}
}
/**
* Computes the target's Score value according to the trusts it has received and the capacity of its trusters in the specified
* trust tree.
*
* @param db A reference to the database
* @param treeOwner The OwnIdentity that owns the trust tree
* @return The new Score if this Identity
* @throws DuplicateScoreException if there already exist more than one {@link Score} objects for the trustee (should never happen)
*/
private synchronized int computeScoreValue(OwnIdentity treeOwner, Identity target) throws DuplicateScoreException {
int value = 0;
ObjectSet receivedTrusts = getReceivedTrusts(target);
while(receivedTrusts.hasNext()) {
Trust trust = receivedTrusts.next();
try {
value += trust.getValue() * (getScore(treeOwner, trust.getTruster())).getCapacity() / 100;
} catch (NotInTrustTreeException e) {}
}
return value;
}
/**
* Computes the target's rank in the trust tree.
* It gets its best ranked truster's rank, plus one. Or -1 if none of its trusters are in the trust tree.
*
* @param db A reference to the database
* @param treeOwner The OwnIdentity that owns the trust tree
* @return The new Rank if this Identity
* @throws DuplicateScoreException if there already exist more than one {@link Score} objects for the trustee (should never happen)
*/
private synchronized int computeRank(OwnIdentity treeOwner, Identity target) throws DuplicateScoreException {
int rank = -1;
ObjectSet receivedTrusts = getReceivedTrusts(target);
while(receivedTrusts.hasNext()) {
Trust trust = receivedTrusts.next();
try {
Score score = getScore(treeOwner, trust.getTruster());
if(score.getCapacity() != 0) // If the truster has no capacity, he can't give his rank
if(rank == -1 || score.getRank() < rank) // If the truster's rank is better than ours or if we have not
rank = score.getRank();
} catch (NotInTrustTreeException e) {}
}
return rank;
}
/* Client interface functions */
public synchronized Identity addIdentity(String requestURI) throws MalformedURLException, InvalidParameterException {
Identity identity;
try {
identity = getIdentityByURI(requestURI);
Logger.debug(this, "Tried to manually add an identity we already know, ignored.");
throw new InvalidParameterException("We already have this identity");
}
catch(UnknownIdentityException e) {
identity = new Identity(new FreenetURI(requestURI), null, false);
storeAndCommit(identity);
Logger.debug(this, "Trying to fetch manually added identity (" + identity.getRequestURI() + ")");
mFetcher.fetch(identity);
}
return identity;
}
public synchronized void deleteIdentity(Identity identity) {
synchronized(mDB.lock()) {
try {
for(Score score : getScores(identity))
mDB.delete(score);
for(Trust trust : getReceivedTrusts(identity))
mDB.delete(trust);
for(Trust givenTrust : getGivenTrusts(identity)) {
mDB.delete(givenTrust);
updateScoreWithoutCommit(givenTrust.getTrustee());
}
mDB.delete(identity);
mDB.commit();
}
catch(RuntimeException e) {
mDB.rollback();
throw e;
}
}
}
public synchronized void deleteIdentity(String id) throws UnknownIdentityException {
synchronized(mDB.lock()) {
Identity identity = getIdentityByID(id);
deleteIdentity(identity);
}
}
public OwnIdentity createOwnIdentity(String nickName, boolean publishTrustList, String context)
throws MalformedURLException, InvalidParameterException {
FreenetURI[] keypair = getPluginRespirator().getHLSimpleClient().generateKeyPair("WoT");
return createOwnIdentity(keypair[0].toString(), keypair[1].toString(), nickName, publishTrustList, context);
}
public synchronized OwnIdentity createOwnIdentity(String insertURI, String requestURI, String nickName,
boolean publishTrustList, String context) throws MalformedURLException, InvalidParameterException {
synchronized(mDB.lock()) {
OwnIdentity identity;
try {
identity = getOwnIdentityByURI(requestURI);
Logger.debug(this, "Tried to create an own identity with an already existing request URI.");
throw new InvalidParameterException("The URI you specified is already used by the own identity " +
identity.getNickname() + ".");
}
catch(UnknownIdentityException uie) {
identity = new OwnIdentity(new FreenetURI(insertURI), new FreenetURI(requestURI), nickName, publishTrustList);
identity.addContext(context);
identity.addContext(IntroductionPuzzle.INTRODUCTION_CONTEXT); /* FIXME: make configureable */
identity.setProperty(IntroductionServer.PUZZLE_COUNT_PROPERTY, Integer.toString(IntroductionServer.DEFAULT_PUZZLE_COUNT));
try {
mDB.store(identity);
initTrustTree(identity);
for(String seedURI : SEED_IDENTITIES) {
try {
setTrustWithoutCommit(identity, getIdentityByURI(seedURI), (byte)100, "I trust the Freenet developers.");
} catch(UnknownIdentityException e) {
Logger.error(this, "SHOULD NOT HAPPEN: Seed identity not known.", e);
}
}
mDB.commit();
Logger.debug(this, "Successfully created a new OwnIdentity (" + identity.getNickname() + ")");
return identity;
}
catch(RuntimeException e) {
mDB.rollback();
throw e;
}
}
}
}
public synchronized void restoreIdentity(String requestURI, String insertURI) throws MalformedURLException, InvalidParameterException {
OwnIdentity identity;
synchronized(mDB.lock()) {
try {
try {
Identity old = getIdentityByURI(requestURI);
if(old instanceof OwnIdentity)
throw new InvalidParameterException("There is already an own identity with the given URI pair.");
// We already have fetched this identity as a stranger's one. We need to update the database.
identity = new OwnIdentity(insertURI, requestURI, old.getNickname(), old.doesPublishTrustList());
/* We re-fetch the current edition to make sure all trustees are imported */
identity.setEdition(old.getEdition() - 1);
identity.setContexts(old.getContexts());
identity.setProperties(old.getProperties());
// Update all received trusts
for(Trust oldReceivedTrust : getReceivedTrusts(old)) {
Trust newReceivedTrust = new Trust(oldReceivedTrust.getTruster(), identity,
oldReceivedTrust.getValue(), oldReceivedTrust.getComment());
mDB.delete(oldReceivedTrust); /* FIXME: Is this allowed by db4o in a for-each loop? */
mDB.store(newReceivedTrust);
}
// Update all received scores
for(Score oldScore : getScores(old)) {
Score newScore = new Score(oldScore.getTreeOwner(), identity, oldScore.getScore(),
oldScore.getRank(), oldScore.getCapacity());
mDB.delete(oldScore);
mDB.store(newScore);
}
mDB.store(identity);
initTrustTree(identity);
// Update all given trusts
for(Trust givenTrust : getGivenTrusts(old)) {
setTrustWithoutCommit(identity, givenTrust.getTrustee(), givenTrust.getValue(), givenTrust.getComment());
/* FIXME: The old code would just delete the old trust value here instead of doing the following...
* Is the following line correct? */
removeTrustWithoutCommit(givenTrust);
}
// Remove the old identity
mDB.delete(old);
storeAndCommit(identity);
Logger.debug(this, "Successfully restored an already known identity from Freenet (" + identity.getNickname() + ")");
} catch (UnknownIdentityException e) {
identity = new OwnIdentity(new FreenetURI(insertURI), new FreenetURI(requestURI), null, false);
// Store the new identity
mDB.store(identity);
initTrustTree(identity);
storeAndCommit(identity);
Logger.debug(this, "Successfully restored not-yet-known identity from Freenet (" + identity.getRequestURI() + ")");
}
}
catch(RuntimeException e) {
mDB.rollback();
throw e;
}
mFetcher.fetch(identity);
}
}
public synchronized void setTrust(String ownTrusterID, String trusteeID, byte value, String comment)
throws UnknownIdentityException, NumberFormatException, InvalidParameterException {
OwnIdentity truster = getOwnIdentityByID(ownTrusterID);
Identity trustee = getIdentityByID(trusteeID);
setTrust(truster, trustee, value, comment);
}
public synchronized void removeTrust(String ownTrusterID, String trusteeID) throws UnknownIdentityException {
OwnIdentity truster = getOwnIdentityByID(ownTrusterID);
Identity trustee = getIdentityByID(trusteeID);
removeTrust(truster, trustee);
}
public synchronized void addContext(String ownIdentityID, String newContext) throws UnknownIdentityException, InvalidParameterException {
Identity identity = getOwnIdentityByID(ownIdentityID);
identity.addContext(newContext);
storeAndCommit(identity);
Logger.debug(this, "Added context '" + newContext + "' to identity '" + identity.getNickname() + "'");
}
public synchronized void removeContext(String ownIdentityID, String context) throws UnknownIdentityException, InvalidParameterException {
Identity identity = getOwnIdentityByID(ownIdentityID);
identity.removeContext(context);
storeAndCommit(identity);
Logger.debug(this, "Removed context '" + context + "' from identity '" + identity.getNickname() + "'");
}
public synchronized String getProperty(String identityID, String property) throws InvalidParameterException, UnknownIdentityException {
return getIdentityByID(identityID).getProperty(property);
}
public synchronized void setProperty(String ownIdentityID, String property, String value)
throws UnknownIdentityException, InvalidParameterException {
Identity identity = getOwnIdentityByID(ownIdentityID);
identity.setProperty(property, value);
storeAndCommit(identity);
Logger.debug(this, "Added property '" + property + "=" + value + "' to identity '" + identity.getNickname() + "'");
}
public void removeProperty(String ownIdentityID, String property) throws UnknownIdentityException, InvalidParameterException {
Identity identity = getOwnIdentityByID(ownIdentityID);
identity.removeProperty(property);
storeAndCommit(identity);
Logger.debug(this, "Removed property '" + property + "' from identity '" + identity.getNickname() + "'");
}
public String getVersion() {
return Version.getMarketingVersion();
}
public long getRealVersion() {
return Version.getRealVersion();
}
public String getString(String key) {
return key;
}
public void setClassLoader(ClassLoader myClassLoader) {
mClassLoader = myClassLoader;
}
public void setLanguage(LANGUAGE newLanguage) {
}
public PluginRespirator getPluginRespirator() {
return mPR;
}
public ExtObjectContainer getDB() {
return mDB;
}
public Config getConfig() {
return mConfig;
}
public IdentityFetcher getIdentityFetcher() {
return mFetcher;
}
public XMLTransformer getXMLTransformer() {
return mXMLTransformer;
}
public IntroductionPuzzleStore getIntroductionPuzzleStore() {
return mPuzzleStore;
}
public IntroductionClient getIntroductionClient() {
return mIntroductionClient;
}
public RequestClient getRequestClient() {
return mRequestClient;
}
}