Implement a better Sone Rescue Mode.
[Sone.git] / src / main / java / net / pterodactylus / sone / core / Core.java
index 53b6700..ea36aad 100644 (file)
@@ -26,6 +26,8 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.Map.Entry;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -38,6 +40,8 @@ import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.fcp.FcpInterface;
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
 import net.pterodactylus.sone.freenet.wot.Identity;
 import net.pterodactylus.sone.freenet.wot.IdentityListener;
 import net.pterodactylus.sone.freenet.wot.IdentityManager;
@@ -49,6 +53,10 @@ import net.pterodactylus.util.config.Configuration;
 import net.pterodactylus.util.config.ConfigurationException;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.number.Numbers;
+import net.pterodactylus.util.thread.Ticker;
+import net.pterodactylus.util.validation.EqualityValidator;
+import net.pterodactylus.util.validation.IntegerRangeValidator;
+import net.pterodactylus.util.validation.OrValidator;
 import net.pterodactylus.util.validation.Validation;
 import net.pterodactylus.util.version.Version;
 import freenet.keys.FreenetURI;
@@ -58,7 +66,7 @@ import freenet.keys.FreenetURI;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Core implements IdentityListener, UpdateListener {
+public class Core implements IdentityListener, UpdateListener, SoneProvider, PostProvider {
 
        /**
         * Enumeration for the possible states of a {@link Sone}.
@@ -107,10 +115,17 @@ public class Core implements IdentityListener, UpdateListener {
        /** The Sone downloader. */
        private final SoneDownloader soneDownloader;
 
+       /** Sone downloader thread-pool. */
+       private final ExecutorService soneDownloaders = Executors.newFixedThreadPool(10);
+
        /** The update checker. */
        private final UpdateChecker updateChecker;
 
+       /** The FCP interface. */
+       private volatile FcpInterface fcpInterface;
+
        /** Whether the core has been stopped. */
+       @SuppressWarnings("unused")
        private volatile boolean stopped;
 
        /** The Sones’ statuses. */
@@ -125,6 +140,10 @@ public class Core implements IdentityListener, UpdateListener {
        /* synchronize access on this on localSones. */
        private final Map<Sone, SoneInserter> soneInserters = new HashMap<Sone, SoneInserter>();
 
+       /** Sone rescuers. */
+       /* synchronize access on this on localSones. */
+       private final Map<Sone, SoneRescuer> soneRescuers = new HashMap<Sone, SoneRescuer>();
+
        /** All local Sones. */
        /* synchronize access on this on itself. */
        private Map<String, Sone> localSones = new HashMap<String, Sone>();
@@ -166,6 +185,9 @@ public class Core implements IdentityListener, UpdateListener {
        /** Trusted identities, sorted by own identities. */
        private Map<OwnIdentity, Set<Identity>> trustedIdentities = Collections.synchronizedMap(new HashMap<OwnIdentity, Set<Identity>>());
 
+       /** Ticker for threads that mark own elements as known. */
+       private Ticker localElementTicker = new Ticker();
+
        /**
         * Creates a new core.
         *
@@ -252,6 +274,16 @@ public class Core implements IdentityListener, UpdateListener {
        }
 
        /**
+        * Sets the FCP interface to use.
+        *
+        * @param fcpInterface
+        *            The FCP interface to use
+        */
+       public void setFcpInterface(FcpInterface fcpInterface) {
+               this.fcpInterface = fcpInterface;
+       }
+
+       /**
         * Returns the status of the given Sone.
         *
         * @param sone
@@ -279,6 +311,26 @@ public class Core implements IdentityListener, UpdateListener {
        }
 
        /**
+        * Returns the Sone rescuer for the given local Sone.
+        *
+        * @param sone
+        *            The local Sone to get the rescuer for
+        * @return The Sone rescuer for the given Sone
+        */
+       public SoneRescuer getSoneRescuer(Sone sone) {
+               Validation.begin().isNotNull("Sone", sone).check().is("Local Sone", isLocalSone(sone)).check();
+               synchronized (localSones) {
+                       SoneRescuer soneRescuer = soneRescuers.get(sone);
+                       if (soneRescuer == null) {
+                               soneRescuer = new SoneRescuer(this, soneDownloader, sone);
+                               soneRescuers.put(sone, soneRescuer);
+                               soneRescuer.start();
+                       }
+                       return soneRescuer;
+               }
+       }
+
+       /**
         * Returns whether the given Sone is currently locked.
         *
         * @param sone
@@ -329,6 +381,7 @@ public class Core implements IdentityListener, UpdateListener {
         * @return The Sone with the given ID, or {@code null} if there is no such
         *         Sone
         */
+       @Override
        public Sone getSone(String id, boolean create) {
                if (isLocalSone(id)) {
                        return getLocalSone(id);
@@ -551,6 +604,7 @@ public class Core implements IdentityListener, UpdateListener {
         *            exists, {@code false} to return {@code null}
         * @return The post, or {@code null} if there is no such post
         */
+       @Override
        public Post getPost(String postId, boolean create) {
                synchronized (posts) {
                        Post post = posts.get(postId);
@@ -831,33 +885,7 @@ public class Core implements IdentityListener, UpdateListener {
                        soneInserters.put(sone, soneInserter);
                        setSoneStatus(sone, SoneStatus.idle);
                        loadSone(sone);
-                       if (!preferences.isSoneRescueMode()) {
-                               soneInserter.start();
-                       }
-                       new Thread(new Runnable() {
-
-                               @Override
-                               @SuppressWarnings("synthetic-access")
-                               public void run() {
-                                       if (!preferences.isSoneRescueMode()) {
-                                               return;
-                                       }
-                                       logger.log(Level.INFO, "Trying to restore Sone from Freenet…");
-                                       coreListenerManager.fireRescuingSone(sone);
-                                       lockSone(sone);
-                                       long edition = sone.getLatestEdition();
-                                       while (!stopped && (edition >= 0) && preferences.isSoneRescueMode()) {
-                                               logger.log(Level.FINE, "Downloading edition " + edition + "…");
-                                               soneDownloader.fetchSone(sone, sone.getRequestUri().setKeyType("SSK").setDocName("Sone-" + edition));
-                                               --edition;
-                                       }
-                                       logger.log(Level.INFO, "Finished restoring Sone from Freenet, starting Inserter…");
-                                       saveSone(sone);
-                                       coreListenerManager.fireRescuedSone(sone);
-                                       soneInserter.start();
-                               }
-
-                       }, "Sone Downloader").start();
+                       soneInserter.start();
                        return sone;
                }
        }
@@ -912,6 +940,7 @@ public class Core implements IdentityListener, UpdateListener {
                                        for (Sone localSone : getLocalSones()) {
                                                if (localSone.getOptions().getBooleanOption("AutoFollow").get()) {
                                                        localSone.addFriend(sone.getId());
+                                                       saveSone(localSone);
                                                }
                                        }
                                }
@@ -919,7 +948,7 @@ public class Core implements IdentityListener, UpdateListener {
                        remoteSones.put(identity.getId(), sone);
                        soneDownloader.addSone(sone);
                        setSoneStatus(sone, SoneStatus.unknown);
-                       new Thread(new Runnable() {
+                       soneDownloaders.execute(new Runnable() {
 
                                @Override
                                @SuppressWarnings("synthetic-access")
@@ -927,7 +956,7 @@ public class Core implements IdentityListener, UpdateListener {
                                        soneDownloader.fetchSone(sone, sone.getRequestUri());
                                }
 
-                       }, "Sone Downloader").start();
+                       });
                        return sone;
                }
        }
@@ -1024,14 +1053,28 @@ public class Core implements IdentityListener, UpdateListener {
        }
 
        /**
-        * Updates the stores Sone with the given Sone.
+        * Updates the stored Sone with the given Sone.
         *
         * @param sone
         *            The updated Sone
         */
        public void updateSone(Sone sone) {
+               updateSone(sone, false);
+       }
+
+       /**
+        * Updates the stored Sone with the given Sone. If {@code soneRescueMode} is
+        * {@code true}, an older Sone than the current Sone can be given to restore
+        * an old state.
+        *
+        * @param sone
+        *            The Sone to update
+        * @param soneRescueMode
+        *            {@code true} if the stored Sone should be updated regardless
+        *            of the age of the given Sone
+        */
+       public void updateSone(Sone sone, boolean soneRescueMode) {
                if (hasSone(sone.getId())) {
-                       boolean soneRescueMode = isLocalSone(sone) && preferences.isSoneRescueMode();
                        Sone storedSone = getSone(sone.getId());
                        if (!soneRescueMode && !(sone.getTime() > storedSone.getTime())) {
                                logger.log(Level.FINE, "Downloaded Sone %s is not newer than stored Sone %s.", new Object[] { sone, storedSone });
@@ -1468,7 +1511,7 @@ public class Core implements IdentityListener, UpdateListener {
                        logger.log(Level.FINE, "Tried to create post for non-local Sone: %s", sone);
                        return null;
                }
-               Post post = new Post(sone, time, text);
+               final Post post = new Post(sone, time, text);
                if (recipient != null) {
                        post.setRecipient(recipient);
                }
@@ -1481,6 +1524,16 @@ public class Core implements IdentityListener, UpdateListener {
                }
                sone.addPost(post);
                saveSone(sone);
+               localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
+
+                       /**
+                        * {@inheritDoc}
+                        */
+                       @Override
+                       public void run() {
+                               markPostKnown(post);
+                       }
+               }, "Mark " + post + " read.");
                return post;
        }
 
@@ -1601,7 +1654,7 @@ public class Core implements IdentityListener, UpdateListener {
                        logger.log(Level.FINE, "Tried to create reply for non-local Sone: %s", sone);
                        return null;
                }
-               Reply reply = new Reply(sone, post, System.currentTimeMillis(), text);
+               final Reply reply = new Reply(sone, post, System.currentTimeMillis(), text);
                synchronized (replies) {
                        replies.put(reply.getId(), reply);
                }
@@ -1611,6 +1664,16 @@ public class Core implements IdentityListener, UpdateListener {
                }
                sone.addReply(reply);
                saveSone(sone);
+               localElementTicker.registerEvent(System.currentTimeMillis() + 10 * 1000, new Runnable() {
+
+                       /**
+                        * {@inheritDoc}
+                        */
+                       @Override
+                       public void run() {
+                               markReplyKnown(reply);
+                       }
+               }, "Mark " + reply + " read.");
                return reply;
        }
 
@@ -1671,6 +1734,9 @@ public class Core implements IdentityListener, UpdateListener {
                        for (SoneInserter soneInserter : soneInserters.values()) {
                                soneInserter.stop();
                        }
+                       for (Sone localSone : localSones.values()) {
+                               saveSone(localSone);
+                       }
                }
                updateChecker.stop();
                updateChecker.removeUpdateListener(this);
@@ -1696,9 +1762,13 @@ public class Core implements IdentityListener, UpdateListener {
                        configuration.getIntValue("Option/ConfigurationVersion").setValue(0);
                        configuration.getIntValue("Option/InsertionDelay").setValue(options.getIntegerOption("InsertionDelay").getReal());
                        configuration.getIntValue("Option/PostsPerPage").setValue(options.getIntegerOption("PostsPerPage").getReal());
+                       configuration.getIntValue("Option/CharactersPerPost").setValue(options.getIntegerOption("CharactersPerPost").getReal());
+                       configuration.getBooleanValue("Option/RequireFullAccess").setValue(options.getBooleanOption("RequireFullAccess").getReal());
                        configuration.getIntValue("Option/PositiveTrust").setValue(options.getIntegerOption("PositiveTrust").getReal());
                        configuration.getIntValue("Option/NegativeTrust").setValue(options.getIntegerOption("NegativeTrust").getReal());
                        configuration.getStringValue("Option/TrustComment").setValue(options.getStringOption("TrustComment").getReal());
+                       configuration.getBooleanValue("Option/ActivateFcpInterface").setValue(options.getBooleanOption("ActivateFcpInterface").getReal());
+                       configuration.getIntValue("Option/FcpFullAccessRequired").setValue(options.getIntegerOption("FcpFullAccessRequired").getReal());
                        configuration.getBooleanValue("Option/SoneRescueMode").setValue(options.getBooleanOption("SoneRescueMode").getReal());
                        configuration.getBooleanValue("Option/ClearOnNextRestart").setValue(options.getBooleanOption("ClearOnNextRestart").getReal());
                        configuration.getBooleanValue("Option/ReallyClearOnNextRestart").setValue(options.getBooleanOption("ReallyClearOnNextRestart").getReal());
@@ -1761,7 +1831,7 @@ public class Core implements IdentityListener, UpdateListener {
        @SuppressWarnings("unchecked")
        private void loadConfiguration() {
                /* create options. */
-               options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new OptionWatcher<Integer>() {
+               options.addIntegerOption("InsertionDelay", new DefaultOption<Integer>(60, new IntegerRangeValidator(0, Integer.MAX_VALUE), new OptionWatcher<Integer>() {
 
                        @Override
                        public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
@@ -1769,10 +1839,29 @@ public class Core implements IdentityListener, UpdateListener {
                        }
 
                }));
-               options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10));
-               options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75));
-               options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25));
+               options.addIntegerOption("PostsPerPage", new DefaultOption<Integer>(10, new IntegerRangeValidator(1, Integer.MAX_VALUE)));
+               options.addIntegerOption("CharactersPerPost", new DefaultOption<Integer>(200, new OrValidator<Integer>(new IntegerRangeValidator(50, Integer.MAX_VALUE), new EqualityValidator<Integer>(-1))));
+               options.addBooleanOption("RequireFullAccess", new DefaultOption<Boolean>(false));
+               options.addIntegerOption("PositiveTrust", new DefaultOption<Integer>(75, new IntegerRangeValidator(0, 100)));
+               options.addIntegerOption("NegativeTrust", new DefaultOption<Integer>(-25, new IntegerRangeValidator(-100, 100)));
                options.addStringOption("TrustComment", new DefaultOption<String>("Set from Sone Web Interface"));
+               options.addBooleanOption("ActivateFcpInterface", new DefaultOption<Boolean>(false, new OptionWatcher<Boolean>() {
+
+                       @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void optionChanged(Option<Boolean> option, Boolean oldValue, Boolean newValue) {
+                               fcpInterface.setActive(newValue);
+                       }
+               }));
+               options.addIntegerOption("FcpFullAccessRequired", new DefaultOption<Integer>(2, new OptionWatcher<Integer>() {
+
+                       @Override
+                       @SuppressWarnings("synthetic-access")
+                       public void optionChanged(Option<Integer> option, Integer oldValue, Integer newValue) {
+                               fcpInterface.setFullAccessRequired(FullAccessRequired.values()[newValue]);
+                       }
+
+               }));
                options.addBooleanOption("SoneRescueMode", new DefaultOption<Boolean>(false));
                options.addBooleanOption("ClearOnNextRestart", new DefaultOption<Boolean>(false));
                options.addBooleanOption("ReallyClearOnNextRestart", new DefaultOption<Boolean>(false));
@@ -1788,11 +1877,15 @@ public class Core implements IdentityListener, UpdateListener {
                        return;
                }
 
-               options.getIntegerOption("InsertionDelay").set(configuration.getIntValue("Option/InsertionDelay").getValue(null));
-               options.getIntegerOption("PostsPerPage").set(configuration.getIntValue("Option/PostsPerPage").getValue(null));
-               options.getIntegerOption("PositiveTrust").set(configuration.getIntValue("Option/PositiveTrust").getValue(null));
-               options.getIntegerOption("NegativeTrust").set(configuration.getIntValue("Option/NegativeTrust").getValue(null));
+               loadConfigurationValue("InsertionDelay");
+               loadConfigurationValue("PostsPerPage");
+               loadConfigurationValue("CharactersPerPost");
+               options.getBooleanOption("RequireFullAccess").set(configuration.getBooleanValue("Option/RequireFullAccess").getValue(null));
+               loadConfigurationValue("PositiveTrust");
+               loadConfigurationValue("NegativeTrust");
                options.getStringOption("TrustComment").set(configuration.getStringValue("Option/TrustComment").getValue(null));
+               options.getBooleanOption("ActivateFcpInterface").set(configuration.getBooleanValue("Option/ActivateFcpInterface").getValue(null));
+               options.getIntegerOption("FcpFullAccessRequired").set(configuration.getIntValue("Option/FcpFullAccessRequired").getValue(null));
                options.getBooleanOption("SoneRescueMode").set(configuration.getBooleanValue("Option/SoneRescueMode").getValue(null));
 
                /* load known Sones. */
@@ -1846,6 +1939,21 @@ public class Core implements IdentityListener, UpdateListener {
        }
 
        /**
+        * Loads an {@link Integer} configuration value for the option with the
+        * given name, logging validation failures.
+        *
+        * @param optionName
+        *            The name of the option to load
+        */
+       private void loadConfigurationValue(String optionName) {
+               try {
+                       options.getIntegerOption(optionName).set(configuration.getIntValue("Option/" + optionName).getValue(null));
+               } catch (IllegalArgumentException iae1) {
+                       logger.log(Level.WARNING, "Invalid value for " + optionName + " in configuration, using default.");
+               }
+       }
+
+       /**
         * Generate a Sone URI from the given URI and latest edition.
         *
         * @param uriString
@@ -1963,6 +2071,7 @@ public class Core implements IdentityListener, UpdateListener {
                }
                synchronized (newSones) {
                        newSones.remove(identity.getId());
+                       coreListenerManager.fireSoneRemoved(sone);
                }
        }
 
@@ -2009,6 +2118,18 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                /**
+                * Validates the given insertion delay.
+                *
+                * @param insertionDelay
+                *            The insertion delay to validate
+                * @return {@code true} if the given insertion delay was valid, {@code
+                *         false} otherwise
+                */
+               public boolean validateInsertionDelay(Integer insertionDelay) {
+                       return options.getIntegerOption("InsertionDelay").validate(insertionDelay);
+               }
+
+               /**
                 * Sets the insertion delay
                 *
                 * @param insertionDelay
@@ -2031,6 +2152,18 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                /**
+                * Validates the number of posts per page.
+                *
+                * @param postsPerPage
+                *            The number of posts per page
+                * @return {@code true} if the number of posts per page was valid,
+                *         {@code false} otherwise
+                */
+               public boolean validatePostsPerPage(Integer postsPerPage) {
+                       return options.getIntegerOption("PostsPerPage").validate(postsPerPage);
+               }
+
+               /**
                 * Sets the number of posts to show per page.
                 *
                 * @param postsPerPage
@@ -2043,6 +2176,62 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                /**
+                * Returns the number of characters per post, or <code>-1</code> if the
+                * posts should not be cut off.
+                *
+                * @return The numbers of characters per post
+                */
+               public int getCharactersPerPost() {
+                       return options.getIntegerOption("CharactersPerPost").get();
+               }
+
+               /**
+                * Validates the number of characters per post.
+                *
+                * @param charactersPerPost
+                *            The number of characters per post
+                * @return {@code true} if the number of characters per post was valid,
+                *         {@code false} otherwise
+                */
+               public boolean validateCharactersPerPost(Integer charactersPerPost) {
+                       return options.getIntegerOption("CharactersPerPost").validate(charactersPerPost);
+               }
+
+               /**
+                * Sets the number of characters per post.
+                *
+                * @param charactersPerPost
+                *            The number of characters per post, or <code>-1</code> to
+                *            not cut off the posts
+                * @return This preferences objects
+                */
+               public Preferences setCharactersPerPost(Integer charactersPerPost) {
+                       options.getIntegerOption("CharactersPerPost").set(charactersPerPost);
+                       return this;
+               }
+
+               /**
+                * Returns whether Sone requires full access to be even visible.
+                *
+                * @return {@code true} if Sone requires full access, {@code false}
+                *         otherwise
+                */
+               public boolean isRequireFullAccess() {
+                       return options.getBooleanOption("RequireFullAccess").get();
+               }
+
+               /**
+                * Sets whether Sone requires full access to be even visible.
+                *
+                * @param requireFullAccess
+                *            {@code true} if Sone requires full access, {@code false}
+                *            otherwise
+                */
+               public void setRequireFullAccess(Boolean requireFullAccess) {
+                       options.getBooleanOption("RequireFullAccess").set(requireFullAccess);
+               }
+
+               /**
                 * Returns the positive trust.
                 *
                 * @return The positive trust
@@ -2052,6 +2241,18 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                /**
+                * Validates the positive trust.
+                *
+                * @param positiveTrust
+                *            The positive trust to validate
+                * @return {@code true} if the positive trust was valid, {@code false}
+                *         otherwise
+                */
+               public boolean validatePositiveTrust(Integer positiveTrust) {
+                       return options.getIntegerOption("PositiveTrust").validate(positiveTrust);
+               }
+
+               /**
                 * Sets the positive trust.
                 *
                 * @param positiveTrust
@@ -2074,6 +2275,18 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                /**
+                * Validates the negative trust.
+                *
+                * @param negativeTrust
+                *            The negative trust to validate
+                * @return {@code true} if the negative trust was valid, {@code false}
+                *         otherwise
+                */
+               public boolean validateNegativeTrust(Integer negativeTrust) {
+                       return options.getIntegerOption("NegativeTrust").validate(negativeTrust);
+               }
+
+               /**
                 * Sets the negative trust.
                 *
                 * @param negativeTrust
@@ -2110,25 +2323,53 @@ public class Core implements IdentityListener, UpdateListener {
                }
 
                /**
-                * Returns whether the rescue mode is active.
+                * Returns whether the {@link FcpInterface FCP interface} is currently
+                * active.
                 *
-                * @return {@code true} if the rescue mode is active, {@code false}
-                *         otherwise
+                * @see FcpInterface#setActive(boolean)
+                * @return {@code true} if the FCP interface is currently active,
+                *         {@code false} otherwise
                 */
-               public boolean isSoneRescueMode() {
-                       return options.getBooleanOption("SoneRescueMode").get();
+               public boolean isFcpInterfaceActive() {
+                       return options.getBooleanOption("ActivateFcpInterface").get();
                }
 
                /**
-                * Sets whether the rescue mode is active.
+                * Sets whether the {@link FcpInterface FCP interface} is currently
+                * active.
                 *
-                * @param soneRescueMode
-                *            {@code true} if the rescue mode is active, {@code false}
-                *            otherwise
+                * @see FcpInterface#setActive(boolean)
+                * @param fcpInterfaceActive
+                *            {@code true} to activate the FCP interface, {@code false}
+                *            to deactivate the FCP interface
+                * @return This preferences object
+                */
+               public Preferences setFcpInterfaceActive(boolean fcpInterfaceActive) {
+                       options.getBooleanOption("ActivateFcpInterface").set(fcpInterfaceActive);
+                       return this;
+               }
+
+               /**
+                * Returns the action level for which full access to the FCP interface
+                * is required.
+                *
+                * @return The action level for which full access to the FCP interface
+                *         is required
+                */
+               public FullAccessRequired getFcpFullAccessRequired() {
+                       return FullAccessRequired.values()[options.getIntegerOption("FcpFullAccessRequired").get()];
+               }
+
+               /**
+                * Sets the action level for which full access to the FCP interface is
+                * required
+                *
+                * @param fcpFullAccessRequired
+                *            The action level
                 * @return This preferences
                 */
-               public Preferences setSoneRescueMode(Boolean soneRescueMode) {
-                       options.getBooleanOption("SoneRescueMode").set(soneRescueMode);
+               public Preferences setFcpFullAccessRequired(FullAccessRequired fcpFullAccessRequired) {
+                       options.getIntegerOption("FcpFullAccessRequired").set((fcpFullAccessRequired != null) ? fcpFullAccessRequired.ordinal() : null);
                        return this;
                }