[483333] [USS] Add terms of use check to login screen

https://bugs.eclipse.org/bugs/show_bug.cgi?id=483333
diff --git a/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/CredentialsDialog.java b/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/CredentialsDialog.java
index 34b5670..a13e7b0 100644
--- a/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/CredentialsDialog.java
+++ b/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/CredentialsDialog.java
@@ -18,6 +18,7 @@
 import org.eclipse.jface.dialogs.Dialog;
 import org.eclipse.jface.dialogs.IDialogConstants;
 import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
 import org.eclipse.swt.layout.GridData;
 import org.eclipse.swt.widgets.Button;
 import org.eclipse.swt.widgets.Composite;
@@ -54,6 +55,12 @@
   }
 
   @Override
+  protected Point getInitialSize()
+  {
+    return CredentialsComposite.INITIAL_SIZE;
+  }
+
+  @Override
   protected void configureShell(Shell newShell)
   {
     super.configureShell(newShell);
@@ -66,7 +73,7 @@
   protected Control createDialogArea(Composite parent)
   {
     setTitle("Log-In");
-    setMessage("Enter the log-in information for your '" + service.getServiceLabel() + "' account.");
+    setMessage("Enter the log-in information for your " + service.getServiceLabel() + " account.");
     initializeDialogUnits(parent);
 
     Composite area = (Composite)super.createDialogArea(parent);
@@ -102,12 +109,43 @@
     super.okPressed();
   }
 
-  protected void validatePage()
+  protected boolean isPageValid()
+  {
+    String termsOfUseLink = service.getTermsOfUseLink();
+    if (!StringUtil.isEmpty(termsOfUseLink))
+    {
+      boolean termsOfUseAgreed = credentialsComposite.isTermsOfUseAgreed();
+      if (!termsOfUseAgreed)
+      {
+        return false;
+      }
+    }
+  
+    Credentials credentials = credentialsComposite.getCredentials();
+    if (credentials == null)
+    {
+      return false;
+    }
+  
+    if (StringUtil.isEmpty(credentials.getUsername()))
+    {
+      return false;
+    }
+  
+    if (StringUtil.isEmpty(credentials.getPassword()))
+    {
+      return false;
+    }
+  
+    return true;
+  }
+
+  private void validatePage()
   {
     if (okButton != null)
     {
-      Credentials credentials = credentialsComposite.getCredentials();
-      okButton.setEnabled(credentials != null && !StringUtil.isEmpty(credentials.getUsername()) && !StringUtil.isEmpty(credentials.getPassword()));
+      boolean valid = isPageValid();
+      okButton.setEnabled(valid);
     }
   }
 }
diff --git a/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/internal/AddServiceDialog.java b/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/internal/AddServiceDialog.java
index 26f2e05..eb0e9cd 100644
--- a/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/internal/AddServiceDialog.java
+++ b/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/internal/AddServiceDialog.java
@@ -46,6 +46,8 @@
 
   private Text recoverPasswordURIText;
 
+  private Text termsOfUseLinkText;
+
   private Button okButton;
 
   private String serviceLabel;
@@ -58,6 +60,8 @@
 
   private URI recoverPasswordURI;
 
+  private String termsOfUseLink;
+
   public AddServiceDialog(Shell parentShell)
   {
     super(parentShell);
@@ -88,6 +92,11 @@
     return recoverPasswordURI;
   }
 
+  public String getTermsOfUseLink()
+  {
+    return termsOfUseLink;
+  }
+
   @Override
   protected void configureShell(Shell newShell)
   {
@@ -150,6 +159,15 @@
     recoverPasswordURIText = new Text(container, SWT.BORDER);
     recoverPasswordURIText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
     recoverPasswordURIText.addModifyListener(this);
+
+    Label termsOfUseLinkLabel = new Label(container, SWT.NONE);
+    termsOfUseLinkLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false));
+    termsOfUseLinkLabel.setText("Terms of Use Link:");
+
+    termsOfUseLinkText = new Text(container, SWT.BORDER);
+    termsOfUseLinkText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+    termsOfUseLinkText.addModifyListener(this);
+
     return area;
   }
 
@@ -238,6 +256,9 @@
       return;
     }
 
+    String text = termsOfUseLinkText.getText();
+    termsOfUseLink = StringUtil.isEmpty(text) ? null : text;
+
     if (StringUtil.isEmpty(serviceLabel))
     {
       okButton.setEnabled(false);
diff --git a/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/internal/CredentialsComposite.java b/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/internal/CredentialsComposite.java
index 229bfdf..5f6eb64 100644
--- a/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/internal/CredentialsComposite.java
+++ b/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/internal/CredentialsComposite.java
@@ -15,14 +15,15 @@
 import org.eclipse.userstorage.internal.StorageService;
 import org.eclipse.userstorage.internal.util.StringUtil;
 
-import org.eclipse.jface.dialogs.MessageDialog;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.events.ModifyEvent;
 import org.eclipse.swt.events.ModifyListener;
 import org.eclipse.swt.events.SelectionAdapter;
 import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
 import org.eclipse.swt.layout.GridData;
 import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Label;
 import org.eclipse.swt.widgets.Link;
@@ -36,6 +37,8 @@
  */
 public class CredentialsComposite extends Composite
 {
+  public static final Point INITIAL_SIZE = new Point(500, 350);
+
   private final Callable<URI> createAccountURIProvider = new Callable<URI>()
   {
     @Override
@@ -79,6 +82,14 @@
 
   private Credentials credentials;
 
+  private boolean termsOfUseAgreed;
+
+  private Button termsOfUseButton;
+
+  private MultiLink termsOfUseMultiLink;
+
+  private Label spacer;
+
   private Label usernameLabel;
 
   private Text usernameText;
@@ -117,36 +128,57 @@
     this.service = service;
     if (service != null)
     {
-      usernameLabel.setEnabled(true);
-      usernameText.setEnabled(true);
-      passwordLabel.setEnabled(true);
-      passwordText.setEnabled(true);
+      String termsOfUse = service.getTermsOfUseLink();
+      if (StringUtil.isEmpty(termsOfUse))
+      {
+        hideTermsOfUse();
+      }
+      else
+      {
+        int columns = getGridColumns();
 
-      enableLink(createAccountLink, createAccountURIProvider);
-      enableLink(editAccountLink, editAccountURIProvider);
-      enableLink(recoverPasswordLink, recoverPasswordURIProvider);
+        termsOfUseButton.setVisible(true);
+        termsOfUseButton.setLayoutData(new GridData(SWT.RIGHT, SWT.TOP, false, false));
+
+        termsOfUseMultiLink.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, true, false, columns - 1, 1));
+        termsOfUseMultiLink.setVisible(true);
+        termsOfUseMultiLink.setText(termsOfUse);
+
+        spacer.setVisible(true);
+        spacer.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false, columns, 1));
+      }
 
       if (showServiceCredentials)
       {
         setCredentials(((StorageService)service).getCredentials());
+        setTermsOfUseAgreed(((StorageService)service).isTermsOfUseAgreed());
       }
     }
     else
     {
-      usernameLabel.setEnabled(false);
-      usernameText.setEnabled(false);
-      passwordLabel.setEnabled(false);
-      passwordText.setEnabled(false);
-
-      createAccountLink.setEnabled(false);
-      editAccountLink.setEnabled(false);
-      recoverPasswordLink.setEnabled(false);
+      hideTermsOfUse();
 
       if (showServiceCredentials)
       {
         setCredentials(null);
+        setTermsOfUseAgreed(false);
       }
     }
+
+    updateEnablement();
+    layout();
+  }
+
+  public boolean isTermsOfUseAgreed()
+  {
+    return termsOfUseAgreed;
+  }
+
+  public void setTermsOfUseAgreed(boolean termsOfUseAgreed)
+  {
+    this.termsOfUseAgreed = termsOfUseAgreed;
+    termsOfUseButton.setSelection(termsOfUseAgreed);
+    updateEnablement();
   }
 
   public Credentials getCredentials()
@@ -188,6 +220,22 @@
 
   protected void createUI(Composite parent, int columns)
   {
+    termsOfUseButton = new Button(parent, SWT.CHECK);
+    termsOfUseButton.addSelectionListener(new SelectionAdapter()
+    {
+      @Override
+      public void widgetSelected(SelectionEvent e)
+      {
+        termsOfUseAgreed = termsOfUseButton.getSelection();
+        updateEnablement();
+        validate();
+      }
+    });
+
+    termsOfUseMultiLink = new MultiLink.ForSystemBrowser(parent, SWT.WRAP);
+    spacer = new Label(parent, SWT.NONE);
+    hideTermsOfUse();
+
     usernameLabel = new Label(parent, SWT.NONE);
     usernameLabel.setText("User name:");
 
@@ -228,10 +276,7 @@
           try
           {
             String uri = uriProvider.call().toString();
-            if (!SystemBrowser.open(uri))
-            {
-              MessageDialog.openInformation(getShell(), "System Browser Not Found", "Go to " + uri + " to " + label.toLowerCase() + ".");
-            }
+            SystemBrowser.openSafe(getShell(), uri, "Go to " + uri + " to " + label.toLowerCase() + ".");
           }
           catch (Exception ex)
           {
@@ -248,12 +293,44 @@
   {
     try
     {
-      URI uri = uriProvider.call();
-      link.setEnabled(uri != null);
+      link.setEnabled(termsOfUseAgreed && uriProvider.call() != null);
     }
     catch (Exception ex)
     {
       //$FALL-THROUGH$
     }
   }
+
+  private void updateEnablement()
+  {
+    usernameLabel.setEnabled(termsOfUseAgreed);
+    usernameText.setEnabled(termsOfUseAgreed);
+    passwordLabel.setEnabled(termsOfUseAgreed);
+    passwordText.setEnabled(termsOfUseAgreed);
+
+    enableLink(createAccountLink, createAccountURIProvider);
+    enableLink(editAccountLink, editAccountURIProvider);
+    enableLink(recoverPasswordLink, recoverPasswordURIProvider);
+  }
+
+  private void hideTermsOfUse()
+  {
+    termsOfUseButton.setVisible(false);
+    termsOfUseButton.setLayoutData(emptyGridData(1, 1));
+
+    termsOfUseMultiLink.setVisible(false);
+    termsOfUseMultiLink.setLayoutData(emptyGridData(getGridColumns() - 1, 1));
+    termsOfUseMultiLink.setText(StringUtil.EMPTY);
+
+    spacer.setVisible(false);
+    spacer.setLayoutData(emptyGridData(getGridColumns(), 1));
+  }
+
+  private static GridData emptyGridData(int horizontalSpan, int verticalSpan)
+  {
+    GridData gridData = new GridData(0, 0);
+    gridData.horizontalSpan = horizontalSpan;
+    gridData.verticalSpan = verticalSpan;
+    return gridData;
+  }
 }
diff --git a/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/internal/MultiLink.java b/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/internal/MultiLink.java
new file mode 100644
index 0000000..ed633b3
--- /dev/null
+++ b/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/internal/MultiLink.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2015 Eike Stepper (Berlin, Germany) and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    Eike Stepper - initial API and implementation
+ */
+package org.eclipse.userstorage.ui.internal;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Link;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author Eike Stepper
+ */
+public abstract class MultiLink extends Composite
+{
+  private static final Pattern PATTERN = Pattern.compile("<a href=[\"']([^\"']+)[\"']>([^<]+)</a>");
+
+  private final Map<String, String> hrefs = new HashMap<String, String>();
+
+  private Link link;
+
+  private String text;
+
+  public MultiLink(Composite parent, int style)
+  {
+    super(parent, SWT.NONE);
+    setLayout(new FillLayout());
+
+    link = new Link(this, style);
+    link.addSelectionListener(new SelectionAdapter()
+    {
+      @Override
+      public void widgetSelected(SelectionEvent e)
+      {
+        String label = e.text;
+
+        String href = hrefs.get(label);
+        if (href != null)
+        {
+          linkSelected(label, href);
+        }
+      }
+    });
+  }
+
+  public String getText()
+  {
+    return text;
+  }
+
+  public void setText(String text)
+  {
+    this.text = text;
+
+    hrefs.clear();
+    StringBuffer buffer = new StringBuffer();
+
+    Matcher matcher = PATTERN.matcher(text);
+    while (matcher.find())
+    {
+      String href = matcher.group(1);
+      String label = matcher.group(2);
+      hrefs.put(label, href);
+      matcher.appendReplacement(buffer, "<a>" + label + "</a>");
+    }
+
+    matcher.appendTail(buffer);
+    link.setText(buffer.toString());
+  }
+
+  @Override
+  public void setEnabled(boolean enabled)
+  {
+    super.setEnabled(enabled);
+    link.setEnabled(enabled);
+  }
+
+  @Override
+  public void setVisible(boolean visible)
+  {
+    super.setVisible(visible);
+    link.setVisible(visible);
+  }
+
+  @Override
+  public boolean setFocus()
+  {
+    return link.setFocus();
+  }
+
+  protected abstract void linkSelected(String label, String href);
+
+  /**
+   * @author Eike Stepper
+   */
+  public static class ForSystemBrowser extends MultiLink
+  {
+    public ForSystemBrowser(Composite parent, int style)
+    {
+      super(parent, style);
+    }
+
+    @Override
+    protected void linkSelected(String label, String href)
+    {
+      SystemBrowser.openSafe(getShell(), href, "Go to " + href + " to read the " + label + ".");
+    }
+  }
+}
diff --git a/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/internal/ServicesPreferencePage.java b/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/internal/ServicesPreferencePage.java
index 5b44b0c..5dc635c 100644
--- a/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/internal/ServicesPreferencePage.java
+++ b/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/internal/ServicesPreferencePage.java
@@ -33,6 +33,7 @@
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.events.SelectionAdapter;
 import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
 import org.eclipse.swt.layout.GridData;
 import org.eclipse.swt.layout.GridLayout;
 import org.eclipse.swt.widgets.Button;
@@ -46,6 +47,7 @@
 import java.net.URI;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Map.Entry;
 
 /**
  * @author Eike Stepper
@@ -58,6 +60,8 @@
 
   private Map<IStorageService, Credentials> credentialsMap = new HashMap<IStorageService, Credentials>();
 
+  private Map<IStorageService, Boolean> termsOfUseAgreedMap = new HashMap<IStorageService, Boolean>();
+
   private TableViewer servicesViewer;
 
   private CredentialsComposite credentialsComposite;
@@ -70,6 +74,8 @@
 
   private IStorageService selectedService;
 
+  private boolean performingDefaults;
+
   public ServicesPreferencePage()
   {
     super("User Storage Service");
@@ -143,10 +149,14 @@
       @Override
       protected void validate()
       {
-        if (selectedService != null)
+        if (selectedService != null && !performingDefaults)
         {
           Credentials credentials = getCredentials();
           credentialsMap.put(selectedService, credentials);
+
+          boolean termsOfUseAgreed = isTermsOfUseAgreed();
+          termsOfUseAgreedMap.put(selectedService, termsOfUseAgreed);
+
           updateEnablement();
         }
       }
@@ -177,8 +187,9 @@
             URI createAccountURI = dialog.getCreateAccountURI();
             URI editAccountURI = dialog.getEditAccountURI();
             URI recoverPasswordURI = dialog.getRecoverPasswordURI();
+            String termsOfUseLink = dialog.getTermsOfUseLink();
 
-            REGISTRY.addService(serviceLabel, serviceURI, createAccountURI, editAccountURI, recoverPasswordURI);
+            REGISTRY.addService(serviceLabel, serviceURI, createAccountURI, editAccountURI, recoverPasswordURI, termsOfUseLink);
           }
         }
       });
@@ -295,15 +306,31 @@
   protected void performDefaults()
   {
     credentialsMap.clear();
+    termsOfUseAgreedMap.clear();
 
-    IStorageService service = selectedService;
-    selectedService = null;
-    setSelectedService(service);
+    try
+    {
+      performingDefaults = true;
+
+      IStorageService service = selectedService;
+      selectedService = null;
+      setSelectedService(service);
+    }
+    finally
+    {
+      performingDefaults = false;
+    }
 
     updateEnablement();
   }
 
   @Override
+  protected Point doComputeSize()
+  {
+    return CredentialsComposite.INITIAL_SIZE;
+  }
+
+  @Override
   public boolean performOk()
   {
     for (Map.Entry<IStorageService, Credentials> entry : credentialsMap.entrySet())
@@ -313,6 +340,13 @@
       ((StorageService)service).setCredentials(credentials);
     }
 
+    for (Entry<IStorageService, Boolean> entry : termsOfUseAgreedMap.entrySet())
+    {
+      IStorageService service = entry.getKey();
+      Boolean termsOfUseAgreed = entry.getValue();
+      ((StorageService)service).setTermsOfUseAgreed(Boolean.TRUE.equals(termsOfUseAgreed));
+    }
+
     updateEnablement();
     return true;
   }
@@ -346,8 +380,16 @@
           }
         }
 
+        Boolean termsOfUseAgreed = termsOfUseAgreedMap.get(selectedService);
+        if (termsOfUseAgreed == null)
+        {
+          termsOfUseAgreed = ((StorageService)selectedService).isTermsOfUseAgreed();
+          termsOfUseAgreedMap.put(selectedService, termsOfUseAgreed);
+        }
+
         credentialsComposite.setService(selectedService);
         credentialsComposite.setCredentials(credentials);
+        credentialsComposite.setTermsOfUseAgreed(termsOfUseAgreed);
 
         if (removeButton != null)
         {
@@ -375,6 +417,7 @@
   private void updateEnablement()
   {
     boolean dirty = false;
+
     for (IStorageService service : REGISTRY.getServices())
     {
       Credentials localCredentials = credentialsMap.get(service);
@@ -406,9 +449,18 @@
       }
     }
 
-    if (testButton != null)
+    if (!dirty)
     {
-      testButton.setEnabled(!dirty);
+      for (IStorageService service : REGISTRY.getServices())
+      {
+        boolean localTermsOfUseAgreed = Boolean.TRUE.equals(termsOfUseAgreedMap.get(service));
+        boolean termsOfUseAgreed = ((StorageService)service).isTermsOfUseAgreed();
+        if (localTermsOfUseAgreed != termsOfUseAgreed)
+        {
+          dirty = true;
+          break;
+        }
+      }
     }
 
     Button defaultsButton = getDefaultsButton();
diff --git a/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/internal/SystemBrowser.java b/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/internal/SystemBrowser.java
index 4a4e239..0021d4b 100644
--- a/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/internal/SystemBrowser.java
+++ b/org.eclipse.userstorage.ui/src/org/eclipse/userstorage/ui/internal/SystemBrowser.java
@@ -12,6 +12,8 @@
 
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.Platform;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.widgets.Shell;
 
 import java.io.File;
 import java.io.IOException;
@@ -32,6 +34,14 @@
 
   private static final String[] LINUX_COMMANDS = { "kde-open", "gnome-open", "xdg-open", "sensible-browser" };
 
+  public static void openSafe(Shell shell, String url, String defaultMessage)
+  {
+    if (!open(url))
+    {
+      MessageDialog.openInformation(shell, "System Browser Not Found", defaultMessage);
+    }
+  }
+
   public static boolean open(String url)
   {
     try
diff --git a/org.eclipse.userstorage/src/org/eclipse/userstorage/IStorageService.java b/org.eclipse.userstorage/src/org/eclipse/userstorage/IStorageService.java
index c9d17c7..cac7bf6 100644
--- a/org.eclipse.userstorage/src/org/eclipse/userstorage/IStorageService.java
+++ b/org.eclipse.userstorage/src/org/eclipse/userstorage/IStorageService.java
@@ -20,7 +20,7 @@
  * Represents a remote <i>user storage service</i> (USS).
  * <p>
  * The {@link Registry storage service registry} makes known storages available and supports the
- * {@link Registry#addService(String, URI, URI, URI, URI) addition} of {@link Dynamic dynamic}
+ * {@link Registry#addService(String, URI, URI, URI, URI, String) addition} of {@link Dynamic dynamic}
  * storages.
  * <p>
  *
@@ -69,6 +69,13 @@
   public URI getRecoverPasswordURI();
 
   /**
+   * Returns the terms of use link of this storage.
+   *
+   * @return the terms of use link of this storage, can be <code>null</code>.<p>
+   */
+  public String getTermsOfUseLink();
+
+  /**
    * Returns a one-permit semaphore that this service acquires while control is passed to a {@link ICredentialsProvider credentials provider},
    * such a a login dialog.
    *
@@ -81,7 +88,7 @@
    * A {@link IStorageService storage service} that is dynamically created as opposed to being
    * statically contributed via the <code>org.eclipse.userstorage.storages</code> extension point).
    * <p>
-   * Dynamic storages can be created and registered via the {@link Registry#addService(String, URI, URI, URI, URI) addStorage()} method
+   * Dynamic storages can be created and registered via the {@link Registry#addService(String, URI, URI, URI, URI, String) addStorage()} method
    * and only dynamic storages can be {@link #remove() removed} from the {@link Registry storage service registry}.
    * <p>
    *
@@ -104,7 +111,7 @@
    * <p>
    * <ul>
    * <li> Static storages that are contributed via the <code>org.eclipse.userstorage.storages</code> extension point.
-   * <li> {@link Dynamic Dynamic} storages that are created via the {@link #addService(String, URI, URI, URI, URI) addStorage()} method.
+   * <li> {@link Dynamic Dynamic} storages that are created via the {@link #addService(String, URI, URI, URI, URI, String) addStorage()} method.
    * </ul>
    * <p>
    * To access the storages in this registry an application uses the {@link #INSTANCE} constant as follows:
@@ -156,14 +163,16 @@
      *        of the user account needed for the REST service behind the storage to be created and registered. See also {@link IStorageService#getEditAccountURI()}.<p>
      * @param recoverPasswordURI an optional (<i>can be</i> <code>null</code>) URI that a user interface can use to point the user to a web page that supports the recovery
      *        of the password needed to log into the REST service behind the storage to be created and registered. See also {@link IStorageService#getRecoverPasswordURI()}.<p>
+     * @param termsOfUseLink an optional (<i>can be</i> <code>null</code>) string that a user interface can use to point the user to a web page that supports the terms of use
+     *        of the REST service behind the storage. See also {@link IStorageService#getTermsOfUseLink()}.<p>
      *
      * @return the newly created and registered storage, never <code>null</code>.<p>
      * @throws IllegalStateException if a storage with the same <code>serviceURI</code> is already registered in this registry.<p>
      *
      * @see #addService(String, URI)
      */
-    public IStorageService.Dynamic addService(String serviceLabel, URI serviceURI, URI createAccountURI, URI editAccountURI, URI recoverPasswordURI)
-        throws IllegalStateException;
+    public IStorageService.Dynamic addService(String serviceLabel, URI serviceURI, URI createAccountURI, URI editAccountURI, URI recoverPasswordURI,
+        String termsOfUseLink) throws IllegalStateException;
 
     /**
      * Adds a new dynamic storage with the given <code>serviceLabel</code> and the given <code>serviceURI</code> to this registry.
@@ -181,7 +190,7 @@
      * @return the newly created and registered storage, never <code>null</code>.<p>
      * @throws IllegalStateException if a storage with the same <code>serviceURI</code> is already registered in this registry.<p>
      *
-     * @see #addService(String, URI, URI, URI, URI)
+     * @see #addService(String, URI, URI, URI, URI, String)
      */
     public IStorageService.Dynamic addService(String serviceLabel, URI serviceURI) throws IllegalStateException;
 
diff --git a/org.eclipse.userstorage/src/org/eclipse/userstorage/internal/StorageService.java b/org.eclipse.userstorage/src/org/eclipse/userstorage/internal/StorageService.java
index d382a78..5e4f0f5 100644
--- a/org.eclipse.userstorage/src/org/eclipse/userstorage/internal/StorageService.java
+++ b/org.eclipse.userstorage/src/org/eclipse/userstorage/internal/StorageService.java
@@ -33,6 +33,8 @@
 
   private static final String PASSWORD_KEY = "password";
 
+  private static final String TERMS_OF_USE_AGREED_KEY = "termsOfUseAgreed";
+
   private final Semaphore authenticationSemaphore = new Semaphore(1);
 
   private final String serviceLabel;
@@ -45,11 +47,13 @@
 
   private final URI recoverPasswordURI;
 
+  private final String termsOfUseLink;
+
   private ICredentialsProvider credentialsProvider;
 
   private Session session;
 
-  public StorageService(String serviceLabel, URI serviceURI, URI createAccountURI, URI editAccountURI, URI recoverPasswordURI)
+  public StorageService(String serviceLabel, URI serviceURI, URI createAccountURI, URI editAccountURI, URI recoverPasswordURI, String termsOfUseLink)
   {
     if (StringUtil.isEmpty(serviceLabel))
     {
@@ -66,6 +70,7 @@
     this.createAccountURI = createAccountURI;
     this.editAccountURI = editAccountURI;
     this.recoverPasswordURI = recoverPasswordURI;
+    this.termsOfUseLink = termsOfUseLink;
   }
 
   @Override
@@ -99,6 +104,12 @@
   }
 
   @Override
+  public String getTermsOfUseLink()
+  {
+    return termsOfUseLink;
+  }
+
+  @Override
   public Semaphore getAuthenticationSemaphore()
   {
     return authenticationSemaphore;
@@ -181,6 +192,62 @@
     }
   }
 
+  public boolean isTermsOfUseAgreed()
+  {
+    try
+    {
+      ISecurePreferences securePreferences = getSecurePreferences();
+      if (securePreferences != null)
+      {
+        String value = securePreferences.get(TERMS_OF_USE_AGREED_KEY, null);
+        if ("true".equalsIgnoreCase(value))
+        {
+          return true;
+        }
+      }
+    }
+    catch (StorageException ex)
+    {
+      Activator.log(ex);
+    }
+
+    return false;
+  }
+
+  public void setTermsOfUseAgreed(boolean agreed)
+  {
+    try
+    {
+      ISecurePreferences securePreferences = getSecurePreferences();
+      if (securePreferences != null)
+      {
+        if (agreed)
+        {
+          securePreferences.putBoolean(TERMS_OF_USE_AGREED_KEY, true, false);
+        }
+        else
+        {
+          securePreferences.remove(USERNAME_KEY);
+          securePreferences.remove(PASSWORD_KEY);
+          securePreferences.remove(TERMS_OF_USE_AGREED_KEY);
+        }
+
+        securePreferences.flush();
+      }
+    }
+    catch (Exception ex)
+    {
+      Activator.log(ex);
+    }
+    finally
+    {
+      if (session != null && !agreed)
+      {
+        session.reset();
+      }
+    }
+  }
+
   public synchronized InputStream retrieveBlob(ICredentialsProvider credentialsProvider, String appToken, String key, Map<String, String> properties,
       boolean useETag) throws IOException
   {
@@ -320,9 +387,9 @@
    */
   public static final class DynamicService extends StorageService implements IStorageService.Dynamic
   {
-    public DynamicService(String serviceLabel, URI serviceURI, URI createAccountURI, URI editAccountURI, URI recoverPasswordURI)
+    public DynamicService(String serviceLabel, URI serviceURI, URI createAccountURI, URI editAccountURI, URI recoverPasswordURI, String termsOfUseLink)
     {
-      super(serviceLabel, serviceURI, createAccountURI, editAccountURI, recoverPasswordURI);
+      super(serviceLabel, serviceURI, createAccountURI, editAccountURI, recoverPasswordURI, termsOfUseLink);
     }
 
     @Override
diff --git a/org.eclipse.userstorage/src/org/eclipse/userstorage/internal/StorageServiceRegistry.java b/org.eclipse.userstorage/src/org/eclipse/userstorage/internal/StorageServiceRegistry.java
index 838deae..386b3b3 100644
--- a/org.eclipse.userstorage/src/org/eclipse/userstorage/internal/StorageServiceRegistry.java
+++ b/org.eclipse.userstorage/src/org/eclipse/userstorage/internal/StorageServiceRegistry.java
@@ -55,6 +55,8 @@
 
   private static final String RECOVER_PASSWORD_URI = "recoverPasswordURI";
 
+  private static final String TERMS_OF_USE_LINK = "termsOfUseLink";
+
   private static final String DEFAULT_SERVICE_LABEL = "Eclipse.org";
 
   private static final String DEFAULT_SERVICE_URI = "https://api.eclipse.org/";
@@ -109,10 +111,10 @@
   }
 
   @Override
-  public IStorageService.Dynamic addService(String serviceLabel, URI serviceURI, URI createAccountURI, URI editAccountURI, URI recoverPasswordURI)
-      throws IllegalStateException
+  public IStorageService.Dynamic addService(String serviceLabel, URI serviceURI, URI createAccountURI, URI editAccountURI, URI recoverPasswordURI,
+      String termsOfUseLink) throws IllegalStateException
   {
-    DynamicService service = new DynamicService(serviceLabel, serviceURI, createAccountURI, editAccountURI, recoverPasswordURI);
+    DynamicService service = new DynamicService(serviceLabel, serviceURI, createAccountURI, editAccountURI, recoverPasswordURI, termsOfUseLink);
     addService(service);
 
     ISecurePreferences securePreferences = service.getSecurePreferences();
@@ -139,7 +141,7 @@
   @Override
   public IStorageService.Dynamic addService(String serviceLabel, URI serviceURI) throws IllegalStateException
   {
-    return addService(serviceLabel, serviceURI, null, null, null);
+    return addService(serviceLabel, serviceURI, null, null, null, null);
   }
 
   @Override
@@ -171,8 +173,9 @@
           URI createAccountURI = StringUtil.newURI(child.get(CREATE_ACCOUNT_URI, null));
           URI editAccountURI = StringUtil.newURI(child.get(EDIT_ACCOUNT_URI, null));
           URI recoverPasswordURI = StringUtil.newURI(child.get(RECOVER_PASSWORD_URI, null));
+          String termsOfUseLink = child.get(TERMS_OF_USE_LINK, null);
 
-          IStorageService.Dynamic service = new DynamicService(serviceLabel, serviceURI, createAccountURI, editAccountURI, recoverPasswordURI);
+          IStorageService.Dynamic service = new DynamicService(serviceLabel, serviceURI, createAccountURI, editAccountURI, recoverPasswordURI, termsOfUseLink);
           addService(service);
           result.add(service);
         }
@@ -308,11 +311,13 @@
           if (serviceURI != null)
           {
             String serviceLabel = System.getProperty(PREFIX + SERVICE_LABEL, DEFAULT_SERVICE_LABEL);
-            URI createAccountURI = getURI(serviceURI, CREATE_ACCOUNT_URI, "https://dev.eclipse.org/site_login/");
-            URI editAccountURI = getURI(serviceURI, EDIT_ACCOUNT_URI, "https://dev.eclipse.org/site_login/myaccount.php");
-            URI recoverPasswordURI = getURI(serviceURI, RECOVER_PASSWORD_URI, "https://dev.eclipse.org/site_login/password_recovery.php");
+            URI createAccountURI = StringUtil.newURI(getValue(serviceURI, CREATE_ACCOUNT_URI, "https://dev.eclipse.org/site_login/"));
+            URI editAccountURI = StringUtil.newURI(getValue(serviceURI, EDIT_ACCOUNT_URI, "https://dev.eclipse.org/site_login/myaccount.php"));
+            URI recoverPasswordURI = StringUtil.newURI(getValue(serviceURI, RECOVER_PASSWORD_URI, "https://dev.eclipse.org/site_login/password_recovery.php"));
+            String termsOfUseLink = getValue(serviceURI, TERMS_OF_USE_LINK,
+                "I agree the use of this beta service is governed by the Eclipse Foundation <a href='http://www.eclipse.org/legal/termsofuse.php'>Terms of Use</a> and the Eclipse Foundation <a href='http://www.eclipse.org/legal/privacy.php'>Privacy Policy</a>.");
 
-            StorageService eclipseStorage = new StorageService(serviceLabel, serviceURI, createAccountURI, editAccountURI, recoverPasswordURI);
+            StorageService eclipseStorage = new StorageService(serviceLabel, serviceURI, createAccountURI, editAccountURI, recoverPasswordURI, termsOfUseLink);
             services.put(eclipseStorage.getServiceURI(), eclipseStorage);
           }
 
@@ -356,19 +361,19 @@
     }
   }
 
-  private static URI getURI(URI serviceURI, String property, String defaultURI)
+  private static String getValue(URI serviceURI, String property, String defaultValue)
   {
-    String uri = System.getProperty(PREFIX + property);
-    if (StringUtil.isEmpty(uri))
+    String value = System.getProperty(PREFIX + property);
+    if (StringUtil.isEmpty(value))
     {
       String authority = serviceURI.getAuthority();
       if (authority != null && authority.endsWith(".eclipse.org"))
       {
-        uri = defaultURI;
+        value = defaultValue;
       }
     }
 
-    return StringUtil.newURI(uri);
+    return value;
   }
 
   private static void setSecurePreference(ISecurePreferences securePreferences, String key, URI uri) throws StorageException
@@ -479,8 +484,9 @@
       URI createAccountURI = StringUtil.newURI(configurationElement.getAttribute(CREATE_ACCOUNT_URI));
       URI editAccountURI = StringUtil.newURI(configurationElement.getAttribute(EDIT_ACCOUNT_URI));
       URI recoverPasswordURI = StringUtil.newURI(configurationElement.getAttribute(RECOVER_PASSWORD_URI));
+      String termsOfUseLink = configurationElement.getAttribute(TERMS_OF_USE_LINK);
 
-      return new StorageService(serviceLabel, serviceURI, createAccountURI, editAccountURI, recoverPasswordURI);
+      return new StorageService(serviceLabel, serviceURI, createAccountURI, editAccountURI, recoverPasswordURI, termsOfUseLink);
     }
   }
 }