In progress: [ Issue 40 ] Add support for import resolution across multiple folders
https://code.google.com/p/protobuf-dt/issues/detail?id=40

Created superclass for pages that are both preference and property page. Cleaning up tests.
diff --git a/com.google.eclipse.protobuf.test/src/com/google/eclipse/protobuf/scoping/ImportUriFixer_fixUri_Test.java b/com.google.eclipse.protobuf.test/src/com/google/eclipse/protobuf/scoping/ImportUriFixer_fixUri_Test.java
index d16bb57..149e7b7 100644
--- a/com.google.eclipse.protobuf.test/src/com/google/eclipse/protobuf/scoping/ImportUriFixer_fixUri_Test.java
+++ b/com.google.eclipse.protobuf.test/src/com/google/eclipse/protobuf/scoping/ImportUriFixer_fixUri_Test.java
@@ -51,8 +51,8 @@
   }
 
   @Test public void should_fix_import_URI_even_if_overlapping_one_folder_only_with_resource_URI() {
-    String fixed = fixer.fixUri("src/proto/read-only/address.proto", resourceUri, resourceChecker);
-    assertThat(fixed, equalTo("platform:/resource/src/proto/read-only/address.proto"));  
+    String uri = fixer.fixUri("src/proto/read-only/address.proto", resourceUri, resourceChecker);
+    assertThat(uri, equalTo("platform:/resource/src/proto/read-only/address.proto"));  
   }
   
   private static class ResourceCheckerStub extends ResourceChecker {
diff --git a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/preferences/CompilerPreferencePage.java b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/preferences/CompilerPreferencePage.java
index a7d7199..fca2eba 100644
--- a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/preferences/CompilerPreferencePage.java
+++ b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/preferences/CompilerPreferencePage.java
@@ -14,20 +14,17 @@
 import static org.eclipse.core.runtime.IStatus.OK;
 
 import java.io.File;
-import java.util.Map;
 
-import org.eclipse.core.resources.*;
-import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.ResourcesPlugin;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.jface.preference.IPreferenceStore;
-import org.eclipse.jface.preference.PreferencePage;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.events.*;
 import org.eclipse.swt.layout.GridData;
 import org.eclipse.swt.layout.GridLayout;
 import org.eclipse.swt.widgets.*;
-import org.eclipse.ui.*;
-import org.eclipse.ui.dialogs.PreferencesUtil;
+import org.eclipse.ui.IWorkbench;
 import org.eclipse.xtext.ui.editor.preferences.IPreferenceStoreAccess;
 
 import com.google.inject.Inject;
@@ -37,12 +34,10 @@
  *
  * @author alruiz@google.com (Alex Ruiz)
  */
-public class CompilerPreferencePage extends PreferencePage implements IWorkbenchPreferencePage, IWorkbenchPropertyPage {
+public class CompilerPreferencePage extends PreferenceAndPropertyPage {
 
   private static final String PREFERENCE_PAGE_ID = "com.google.eclipse.protobuf.ui.preferences.CompilerPreferencePage";
 
-  private Button btnEnableProjectSettings;
-  private Link lnkEnableWorkspaceSettings;
   private Button btnCompileProtoFiles;
   private TabFolder tabFolder;
   private TabItem tbtmMain;
@@ -65,35 +60,16 @@
   private Button btnRefreshOutputFolder;
   private Label lblOutputFolderRelative;
 
-  private IProject project;
-
-  private final IPreferenceStoreAccess preferenceStoreAccess;
-
-  private Map<String, Object> dataMap;
-
   @Inject public CompilerPreferencePage(IPreferenceStoreAccess preferenceStoreAccess) {
-    this.preferenceStoreAccess = preferenceStoreAccess;
+    super(preferenceStoreAccess);
   }
 
   /** {@inheritDoc} */
   @Override protected Control createContents(Composite parent) {
     // generated by WindowBuilder
-    Composite contents = new Composite(parent, NONE);
+    Composite contents = contentsComposite(parent);
     contents.setLayout(new GridLayout(3, false));
 
-    if (isPropertyPage()) {
-      btnEnableProjectSettings = new Button(contents, SWT.CHECK);
-      btnEnableProjectSettings.setText(CompilerPreferencePage_enableProjectSettings);
-
-      lnkEnableWorkspaceSettings = new Link(contents, SWT.NONE);
-      lnkEnableWorkspaceSettings.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
-      lnkEnableWorkspaceSettings.setText("<a>" + CompilerPreferencePage_configureWorkspaceSettings + "</a>");
-
-      Label label = new Label(contents, SWT.SEPARATOR | SWT.HORIZONTAL);
-      label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1));
-    }
-    new Label(contents, SWT.NONE);
-
     btnCompileProtoFiles = new Button(contents, SWT.CHECK);
     btnCompileProtoFiles.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, false, 2, 1));
     btnCompileProtoFiles.setText(CompilerPreferencePage_compileOnSave);
@@ -127,6 +103,8 @@
 
     btnProtocPathBrowse = new Button(grpCompilerLocation, SWT.NONE);
     btnProtocPathBrowse.setText(CompilerPreferencePage_browseCustomPath);
+    new Label(grpCompilerLocation, SWT.NONE);
+    new Label(grpCompilerLocation, SWT.NONE);
 
     grpTargetLanguage = new Group(cmpMain, SWT.NONE);
     grpTargetLanguage.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
@@ -157,7 +135,7 @@
 
     lblOutputFolderRelative = new Label(grpOutput, SWT.NONE);
     lblOutputFolderRelative.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1));
-    lblOutputFolderRelative.setText("* relative to the project's folder");
+    lblOutputFolderRelative.setText(CompilerPreferencePage_directChildOfProjectFolder);
 
     tbtmRefresh = new TabItem(tabFolder, SWT.NONE);
     tbtmRefresh.setText(CompilerPreferencePage_refreshTab);
@@ -181,6 +159,7 @@
     btnRefreshOutputFolder = new Button(grpRefresh, SWT.RADIO);
     btnRefreshOutputFolder.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
     btnRefreshOutputFolder.setText(CompilerPreferencePage_refreshOutputProject);
+    new Label(contents, SWT.NONE);
 
     updateFromPreferenceStore();
     addEventListeners();
@@ -205,8 +184,8 @@
     boolean enableCompilerOptions = compileProtoFiles;
     if (isPropertyPage()) {
       boolean useProjectSettings = store.getBoolean(ENABLE_PROJECT_SETTINGS);
-      btnEnableProjectSettings.setSelection(useProjectSettings);
-      setPropertySpecificOptionsEnabled(useProjectSettings);
+      activateProjectSettings(useProjectSettings);
+      setProjectSpecificOptionsEnabled(useProjectSettings);
       enableCompilerOptions = enableCompilerOptions && useProjectSettings;
     }
     setCompilerOptionsEnabled(enableCompilerOptions);
@@ -253,25 +232,6 @@
     };
     txtProtocFilePath.addModifyListener(modifyListener);
     txtOutputFolderName.addModifyListener(modifyListener);
-    if (isPropertyPage()) {
-      btnEnableProjectSettings.addSelectionListener(new SelectionAdapter() {
-        @Override public void widgetSelected(SelectionEvent e) {
-          boolean useProjectSettings = btnEnableProjectSettings.getSelection();
-          setPropertySpecificOptionsEnabled(useProjectSettings);
-          setCompilerOptionsEnabled(isEnabledAndSelected(btnCompileProtoFiles));
-        }
-      });
-      lnkEnableWorkspaceSettings.addSelectionListener(new SelectionAdapter() {
-        @Override public void widgetSelected(SelectionEvent e) {
-          openWorkspacePreferences(dataMap);
-        }
-      });
-    }
-  }
-
-  private void openWorkspacePreferences(Object data) {
-    String id = PREFERENCE_PAGE_ID;
-    PreferencesUtil.createPreferenceDialogOn(getShell(), id, new String[] { id }, data).open();
   }
 
   private void checkState() {
@@ -331,17 +291,16 @@
     boolean enableCompilerOptions = compileProtoFiles;
     if (isPropertyPage()) {
       boolean useProjectSettings = store.getDefaultBoolean(ENABLE_PROJECT_SETTINGS);
-      btnEnableProjectSettings.setSelection(useProjectSettings);
-      setPropertySpecificOptionsEnabled(useProjectSettings);
+      activateProjectSettings(useProjectSettings);
+      setProjectSpecificOptionsEnabled(useProjectSettings);
       enableCompilerOptions = enableCompilerOptions && useProjectSettings;
     }
     setCompilerOptionsEnabled(enableCompilerOptions);
     super.performDefaults();
   }
 
-  private void setPropertySpecificOptionsEnabled(boolean enabled) {
+  private void setProjectSpecificOptionsEnabled(boolean enabled) {
     btnCompileProtoFiles.setEnabled(enabled);
-    lnkEnableWorkspaceSettings.setEnabled(!enabled);
   }
 
   private void setCompilerOptionsEnabled(boolean enabled) {
@@ -408,7 +367,7 @@
   private void savePreferences() {
     IPreferenceStore store = getPreferenceStore();
     if (isPropertyPage()) {
-      store.setValue(ENABLE_PROJECT_SETTINGS, btnEnableProjectSettings.getSelection());
+      store.setValue(ENABLE_PROJECT_SETTINGS, areProjectSettingsActive());
     }
     store.setValue(COMPILE_PROTO_FILES, btnCompileProtoFiles.getSelection());
     store.setValue(USE_PROTOC_IN_SYSTEM_PATH, btnUseProtocInSystemPath.getSelection());
@@ -422,34 +381,15 @@
     store.setValue(REFRESH_PROJECT, btnRefreshProject.getSelection());
     store.setValue(REFRESH_OUTPUT_FOLDER, btnRefreshOutputFolder.getSelection());
   }
-
+  
   /** {@inheritDoc} */
-  public IAdaptable getElement() {
-    return project;
+  @Override protected void onProjectSettingsActivation(boolean projectSettingsEnabled) {
+    setProjectSpecificOptionsEnabled(projectSettingsEnabled);
+    setCompilerOptionsEnabled(isEnabledAndSelected(btnCompileProtoFiles));
   }
-
+  
   /** {@inheritDoc} */
-  public void setElement(IAdaptable element) {
-    this.project = (IProject) element.getAdapter(IProject.class);
-  }
-
-  @Override protected IPreferenceStore doGetPreferenceStore() {
-    if (isPropertyPage()) return preferenceStoreAccess.getWritablePreferenceStore(currentProject());
-    return preferenceStoreAccess.getWritablePreferenceStore();
-  }
-
-  private boolean isPropertyPage() {
-    return project != null;
-  }
-
-  private IProject currentProject() {
-    if (project == null)
-      throw new IllegalStateException("Not a property page case, but current project was requested.");
-    return project;
-  }
-
-  /** {@inheritDoc} */
-  @SuppressWarnings("unchecked") @Override public void applyData(Object data) {
-    if (data instanceof Map) this.dataMap = (Map<String, Object>) data;
+  @Override protected String preferencePageId() {
+    return PREFERENCE_PAGE_ID;
   }
 }
diff --git a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/preferences/Messages.java b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/preferences/Messages.java
index 89a0673..3940ab3 100644
--- a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/preferences/Messages.java
+++ b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/preferences/Messages.java
@@ -22,8 +22,9 @@
 
   private Messages() {}
 
-  public static String CompilerPreferencePage_enableProjectSettings;
-  public static String CompilerPreferencePage_configureWorkspaceSettings;
+  public static String BasePreferencePage_enableProjectSettings;
+  public static String BasePreferencePage_configureWorkspaceSettings;
+  
   public static String CompilerPreferencePage_mainTab;
   public static String CompilerPreferencePage_refreshTab;
   public static String CompilerPreferencePage_browseCustomPath;
@@ -37,6 +38,7 @@
   public static String CompilerPreferencePage_generatePython;
   public static String CompilerPreferencePage_generatedCode;
   public static String CompilerPreferencePage_outputFolderName;
+  public static String CompilerPreferencePage_directChildOfProjectFolder;
   public static String CompilerPreferencePage_refreshResources;
   public static String CompilerPreferencePage_refreshProject;
   public static String CompilerPreferencePage_refreshOutputProject;
diff --git a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/preferences/Messages.properties b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/preferences/Messages.properties
index b69400c..def9ed8 100644
--- a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/preferences/Messages.properties
+++ b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/preferences/Messages.properties
@@ -1,5 +1,6 @@
-CompilerPreferencePage_enableProjectSettings=Enable project specific settings
-CompilerPreferencePage_configureWorkspaceSettings=Configure Workspace Settings...
+BasePreferencePage_enableProjectSettings=Enable project specific settings
+BasePreferencePage_configureWorkspaceSettings=Configure Workspace Settings...
+
 CompilerPreferencePage_mainTab=&Main
 CompilerPreferencePage_refreshTab=&Refresh
 CompilerPreferencePage_browseCustomPath=&Browse...
@@ -13,6 +14,7 @@
 CompilerPreferencePage_generatePython=&Python
 CompilerPreferencePage_generatedCode=Generated Code
 CompilerPreferencePage_outputFolderName=Folder Name: *
+CompilerPreferencePage_directChildOfProjectFolder=* Direct child of project folder
 CompilerPreferencePage_refreshResources=Refresh resources upon completion.
 CompilerPreferencePage_refreshProject=Project
 CompilerPreferencePage_refreshOutputProject=Folder containing generated code
diff --git a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/preferences/PreferenceAndPropertyPage.java b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/preferences/PreferenceAndPropertyPage.java
new file mode 100644
index 0000000..04481f4
--- /dev/null
+++ b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/preferences/PreferenceAndPropertyPage.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2011 Google Inc.
+ *
+ * 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
+ */
+package com.google.eclipse.protobuf.ui.preferences;
+
+import static com.google.eclipse.protobuf.ui.preferences.Messages.*;
+import static org.eclipse.ui.dialogs.PreferencesUtil.createPreferenceDialogOn;
+
+import java.util.Map;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.preference.PreferencePage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.*;
+import org.eclipse.ui.IWorkbenchPreferencePage;
+import org.eclipse.ui.IWorkbenchPropertyPage;
+import org.eclipse.xtext.ui.editor.preferences.IPreferenceStoreAccess;
+
+import com.google.inject.Inject;
+
+/**
+ * Base class for pages that set up both "Workspace Preferences" and "Project Properties."
+ * 
+ * @author alruiz@google.com (Alex Ruiz)
+ */
+public abstract class PreferenceAndPropertyPage extends PreferencePage implements IWorkbenchPreferencePage, IWorkbenchPropertyPage {
+
+  private Button btnEnableProjectSettings;
+  private Link lnkEnableWorkspaceSettings;
+
+  private IProject project;
+  private Map<String, Object> dataMap;
+
+  private final IPreferenceStoreAccess preferenceStoreAccess;
+
+  @Inject public PreferenceAndPropertyPage(IPreferenceStoreAccess preferenceStoreAccess) {
+    this.preferenceStoreAccess = preferenceStoreAccess;
+  }
+
+  /**
+   * Creates the <code>{@link Composite}</code> to be used as the base container in implementations of 
+   * <code>{@link #createContents(Composite)}</code>.
+   * @param parent the parent {@code Composite}.
+   * @return the created {@code Composite}.
+   */
+  protected final Composite contentsComposite(Composite parent) {
+    // generated by WindowBuilder
+    Composite contents = new Composite(parent, NONE);
+    contents.setLayout(new GridLayout(3, false));
+    if (isPropertyPage()) {
+      btnEnableProjectSettings = new Button(contents, SWT.CHECK);
+      btnEnableProjectSettings.setText(BasePreferencePage_enableProjectSettings);
+
+      lnkEnableWorkspaceSettings = new Link(contents, SWT.NONE);
+      lnkEnableWorkspaceSettings.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
+      lnkEnableWorkspaceSettings.setText("<a>" + BasePreferencePage_configureWorkspaceSettings + "</a>");
+
+      Label label = new Label(contents, SWT.SEPARATOR | SWT.HORIZONTAL);
+      label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1));
+    }
+    new Label(contents, SWT.NONE);
+    addEventListeners();
+    return contents;
+  }
+  
+  private void addEventListeners() {
+    if (isPropertyPage()) {
+      btnEnableProjectSettings.addSelectionListener(new SelectionAdapter() {
+        @Override public void widgetSelected(SelectionEvent e) {
+          onProjectSettingsActivation(areProjectSettingsActive());
+          updateEnableWorkspaceSettingsLink();
+        }
+      });
+      lnkEnableWorkspaceSettings.addSelectionListener(new SelectionAdapter() {
+        @Override public void widgetSelected(SelectionEvent e) {
+          openWorkspacePreferences();
+        }
+      });
+    }
+  }
+
+  /**
+   * Notification that the "Enable project specific settings" check button has been selected. 
+   * @param projectSettingsActive indicates the selection of the "Enable project specific settings" check button.
+   */
+  protected abstract void onProjectSettingsActivation(boolean projectSettingsActive);
+
+  private void openWorkspacePreferences() {
+    String preferencePageId = preferencePageId();
+    createPreferenceDialogOn(getShell(), preferencePageId , new String[] { preferencePageId }, dataMap).open();
+  }
+
+  /**
+   * Returns the id of this preference page.
+   * @return the id of this preference page.
+   */
+  protected abstract String preferencePageId();
+  
+  /**
+   * Returns the <code>{@link IProject}</code> that owns the properties shown in this page.
+   * @return the project that owns the properties shown in this page.
+   */
+  public final IAdaptable getElement() {
+    return project;
+  }
+
+  /**
+   * Sets the <code>{@link IProject}</code> that owns the properties shown in this page.
+   * @param element the {@code IAdaptable} associated with the project that owns the properties shown in this page.
+   */
+  public final void setElement(IAdaptable element) {
+    this.project = (IProject) element.getAdapter(IProject.class);
+  }
+
+  /**
+   * Returns the preference store of this preference page.
+   * @return the preference store.
+   */
+  @Override protected final IPreferenceStore doGetPreferenceStore() {
+    if (isPropertyPage()) return preferenceStoreAccess.getWritablePreferenceStore(currentProject());
+    return preferenceStoreAccess.getWritablePreferenceStore();
+  }
+
+  /**
+   * Indicates whether this page is a "Project Properties" page or not.
+   * @return {@code true} if this page is a "Project Properties" page, or {@code false} if this page is a 
+   * "Worspace Settings" page.
+   */
+  protected final boolean isPropertyPage() {
+    return project != null;
+  }
+
+  private IProject currentProject() {
+    if (project == null)
+      throw new IllegalStateException("Not a property page case, but current project was requested.");
+    return project;
+  }
+
+  /** {@inheritDoc} */
+  @SuppressWarnings("unchecked") 
+  @Override public final void applyData(Object data) {
+    if (data instanceof Map) this.dataMap = (Map<String, Object>) data;
+  }
+  
+  /**
+   * Activates or deactivates the project-specific settings. 
+   * @param active indicates whether the project-specific settings should be active or not.
+   */
+  protected final void activateProjectSettings(boolean active) {
+    btnEnableProjectSettings.setSelection(active);
+    updateEnableWorkspaceSettingsLink();
+  }
+  
+  private void updateEnableWorkspaceSettingsLink() {
+    lnkEnableWorkspaceSettings.setEnabled(!areProjectSettingsActive());
+  }
+  
+  /**
+   * Indicates if the project-specific settings are active or not.
+   * @return {@code true} if the project-specific settings are active; {@code false} otherwise.
+   */
+  protected final boolean areProjectSettingsActive() {
+    return btnEnableProjectSettings.getSelection();
+  }
+}
diff --git a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/preferences/TargetLanguage.java b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/preferences/TargetLanguage.java
index a1dcfef..af4d49b 100644
--- a/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/preferences/TargetLanguage.java
+++ b/com.google.eclipse.protobuf.ui/src/com/google/eclipse/protobuf/ui/preferences/TargetLanguage.java
@@ -21,6 +21,7 @@
 
   JAVA, CPP, PYTHON;
 
+  // TODO check if protoc can generate more than one language sources at the same time.
   static TargetLanguage find(IPreferenceStore store) {
     if (store.getBoolean(GENERATE_JAVA_CODE)) return JAVA;
     if (store.getBoolean(GENERATE_CPP_CODE)) return CPP;