From da0269b4bd57bf90877446d9f991247bc1ad2f64 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Wed, 12 Oct 2011 22:14:10 -0400
Subject: [PATCH] RPC Client: Create/Edit Repository & User. Partially working.

---
 docs/02_rpc.mkd                                   |    1 
 src/com/gitblit/client/GitblitClient.java         |    8 
 src/com/gitblit/client/GitblitPanel.java          |  399 ++++++++++++++++++-
 src/com/gitblit/utils/JsonUtils.java              |   19 
 src/com/gitblit/utils/RpcUtils.java               |   61 ++
 src/com/gitblit/client/EditUserDialog.java        |  237 +++++++++++
 src/com/gitblit/RpcServlet.java                   |   12 
 src/com/gitblit/client/GitblitClientLauncher.java |    8 
 src/com/gitblit/client/JPalette.java              |  177 ++++++++
 src/com/gitblit/wicket/GitBlitWebApp.properties   |    4 
 tests/com/gitblit/tests/RpcTests.java             |    6 
 src/com/gitblit/client/NameRenderer.java          |    4 
 src/com/gitblit/client/EditRepositoryDialog.java  |  224 +++++++++++
 src/com/gitblit/Constants.java                    |    2 
 14 files changed, 1,125 insertions(+), 37 deletions(-)

diff --git a/docs/02_rpc.mkd b/docs/02_rpc.mkd
index c3c59e6..4a547d0 100644
--- a/docs/02_rpc.mkd
+++ b/docs/02_rpc.mkd
@@ -30,6 +30,7 @@
 <tr><td>LIST_FEDERATION_RESULTS</td><td>-</td><td><em>admin</em></td><td>-</td><td>List FederationModel</td></tr>
 <tr><td>LIST_FEDERATION_PROPOSALS</td><td>-</td><td><em>admin</em></td><td>-</td><td>List FederationProposal </td></tr>
 <tr><td>LIST_FEDERATION_SETS</td><td>-</td><td><em>admin</em></td><td>-</td><td>List FederationSet </td></tr>
+<tr><td>LIST_SETTINGS</td><td>-</td><td><em>admin</em></td><td>-</td><td>Properties</td></tr>
 </table>
 
 ### RPC Client
diff --git a/src/com/gitblit/Constants.java b/src/com/gitblit/Constants.java
index 52af3e9..6300fa2 100644
--- a/src/com/gitblit/Constants.java
+++ b/src/com/gitblit/Constants.java
@@ -204,7 +204,7 @@
 		LIST_REPOSITORIES, CREATE_REPOSITORY, EDIT_REPOSITORY, DELETE_REPOSITORY,
 		LIST_USERS, CREATE_USER, EDIT_USER, DELETE_USER, LIST_REPOSITORY_MEMBERS,
 		SET_REPOSITORY_MEMBERS, LIST_FEDERATION_REGISTRATIONS, LIST_FEDERATION_RESULTS,
-		LIST_FEDERATION_PROPOSALS, LIST_FEDERATION_SETS;
+		LIST_FEDERATION_PROPOSALS, LIST_FEDERATION_SETS, LIST_SETTINGS;
 
 		public static RpcRequest fromName(String name) {
 			for (RpcRequest type : values()) {
diff --git a/src/com/gitblit/RpcServlet.java b/src/com/gitblit/RpcServlet.java
index 4dee319..de5d94e 100644
--- a/src/com/gitblit/RpcServlet.java
+++ b/src/com/gitblit/RpcServlet.java
@@ -22,6 +22,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Properties;
 
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
@@ -163,6 +164,17 @@
 			} else {
 				response.sendError(HttpServletResponse.SC_FORBIDDEN);
 			}
+		} else if (RpcRequest.LIST_SETTINGS.equals(reqType)) {
+			// return the server's settings
+			Properties settings = new Properties();			
+			List<String> keys = GitBlit.getAllKeys(null);
+			for (String key:keys) {
+				String value = GitBlit.getString(key, null);
+				if (value != null) {
+					settings.put(key, value);
+				}
+			}
+			result = settings;
 		}
 
 		// send the result of the request
diff --git a/src/com/gitblit/client/EditRepositoryDialog.java b/src/com/gitblit/client/EditRepositoryDialog.java
new file mode 100644
index 0000000..756128a
--- /dev/null
+++ b/src/com/gitblit/client/EditRepositoryDialog.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.List;
+
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Constants.FederationStrategy;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+
+public class EditRepositoryDialog extends JDialog {
+
+	private static final long serialVersionUID = 1L;
+
+	private final RepositoryModel repository;
+
+	private boolean canceled = true;
+
+	private JTextField nameField;
+
+	private JTextField descriptionField;
+
+	private JCheckBox useTickets;
+
+	private JCheckBox useDocs;
+
+	private JCheckBox showRemoteBranches;
+
+	private JCheckBox showReadme;
+
+	private JCheckBox isFrozen;
+
+	private JComboBox accessRestriction;
+
+	private JComboBox federationStrategy;
+
+	private JComboBox owner;
+
+	private JPalette<String> usersPalette;
+
+	private JPalette<String> setsPalette;
+
+	public EditRepositoryDialog(List<UserModel> allusers) {
+		this(new RepositoryModel(), allusers);
+		setTitle("Create Repository");
+	}
+
+	public EditRepositoryDialog(RepositoryModel aRepository, List<UserModel> allUsers) {
+		super();
+		this.repository = new RepositoryModel();
+		initialize(aRepository, allUsers);
+		setModal(true);
+		setTitle("Edit Repository: " + aRepository.name);
+		setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage());
+	}
+
+	private void initialize(RepositoryModel anRepository, List<UserModel> allUsers) {
+		nameField = new JTextField(anRepository.name == null ? "" : anRepository.name, 35);
+		descriptionField = new JTextField(anRepository.description == null ? ""
+				: anRepository.description, 35);
+
+		owner = new JComboBox(allUsers.toArray());
+		if (!StringUtils.isEmpty(anRepository.owner)) {
+			UserModel currentOwner = null;
+			for (UserModel user : allUsers) {
+				if (user.username.equalsIgnoreCase(anRepository.owner)) {
+					currentOwner = user;
+					break;
+				}
+			}
+			owner.setSelectedItem(currentOwner);
+		}
+
+		useTickets = new JCheckBox("distributed Ticgit issues", anRepository.useTickets);
+		useDocs = new JCheckBox("enumerates Markdown documentation in repository",
+				anRepository.useDocs);
+		showRemoteBranches = new JCheckBox("show remote branches", anRepository.showRemoteBranches);
+		showReadme = new JCheckBox("show a \"readme\" Markdown file on the summary page",
+				anRepository.showReadme);
+		isFrozen = new JCheckBox("deny push operations", anRepository.isFrozen);
+
+		accessRestriction = new JComboBox(AccessRestrictionType.values());
+		accessRestriction.setSelectedItem(anRepository.accessRestriction);
+
+		federationStrategy = new JComboBox(FederationStrategy.values());
+		federationStrategy.setSelectedItem(anRepository.federationStrategy);
+
+		JPanel fieldsPanel = new JPanel(new GridLayout(0, 1));
+		fieldsPanel.add(newFieldPanel("name", nameField));
+		fieldsPanel.add(newFieldPanel("description", descriptionField));
+		fieldsPanel.add(newFieldPanel("owner", owner));
+
+		fieldsPanel.add(newFieldPanel("enable tickets", useTickets));
+		fieldsPanel.add(newFieldPanel("enable docs", useDocs));
+		fieldsPanel.add(newFieldPanel("show remote branches", showRemoteBranches));
+		fieldsPanel.add(newFieldPanel("show readme", showReadme));
+		fieldsPanel.add(newFieldPanel("is frozen", isFrozen));
+
+		usersPalette = new JPalette<String>();
+		JPanel accessPanel = new JPanel(new BorderLayout(5, 5));
+		accessPanel.add(newFieldPanel("access restriction", accessRestriction), BorderLayout.NORTH);
+		accessPanel.add(newFieldPanel("permitted users", usersPalette), BorderLayout.CENTER);
+
+		setsPalette = new JPalette<String>();
+		JPanel federationPanel = new JPanel(new BorderLayout(5, 5));
+		federationPanel.add(newFieldPanel("federation strategy", federationStrategy),
+				BorderLayout.NORTH);
+		federationPanel.add(newFieldPanel("federation sets", setsPalette), BorderLayout.CENTER);
+
+		JPanel panel = new JPanel(new BorderLayout(5, 5));
+		panel.add(fieldsPanel, BorderLayout.NORTH);
+		panel.add(accessPanel, BorderLayout.CENTER);
+		panel.add(federationPanel, BorderLayout.SOUTH);
+
+		JButton createButton = new JButton("Save");
+		createButton.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent event) {
+				if (validateFields()) {
+					canceled = false;
+					setVisible(false);
+				}
+			}
+		});
+
+		JButton cancelButton = new JButton("Cancel");
+		cancelButton.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent event) {
+				canceled = true;
+				setVisible(false);
+			}
+		});
+
+		JPanel controls = new JPanel();
+		controls.add(cancelButton);
+		controls.add(createButton);
+
+		final Insets _insets = new Insets(5, 5, 5, 5);
+		JPanel centerPanel = new JPanel(new BorderLayout(5, 5)) {
+
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			public Insets getInsets() {
+				return _insets;
+			}
+		};
+		centerPanel.add(panel, BorderLayout.CENTER);
+		centerPanel.add(controls, BorderLayout.SOUTH);
+
+		getContentPane().setLayout(new BorderLayout(5, 5));
+		getContentPane().add(centerPanel, BorderLayout.CENTER);
+		pack();
+		setLocationRelativeTo(null);
+	}
+
+	private JPanel newFieldPanel(String label, JComponent comp) {
+		JLabel fieldLabel = new JLabel(label);
+		fieldLabel.setFont(fieldLabel.getFont().deriveFont(Font.BOLD));
+		fieldLabel.setPreferredSize(new Dimension(150, 20));
+		JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
+		panel.add(fieldLabel);
+		panel.add(comp);
+		return panel;
+	}
+
+	private boolean validateFields() {
+		// TODO validate input and populate model
+		return true;
+	}
+
+	private void showValidationError(String message) {
+		JOptionPane.showMessageDialog(EditRepositoryDialog.this, message, "Validation Error",
+				JOptionPane.ERROR_MESSAGE);
+	}
+
+	public void setUsers(List<String> all, List<String> selected) {
+		usersPalette.setObjects(all, selected);
+	}
+
+	public void setFederationSets(List<String> all, List<String> selected) {
+		setsPalette.setObjects(all, selected);
+	}
+
+	public RepositoryModel getRepository() {
+		if (canceled) {
+			return null;
+		}
+		return repository;
+	}
+}
diff --git a/src/com/gitblit/client/EditUserDialog.java b/src/com/gitblit/client/EditUserDialog.java
new file mode 100644
index 0000000..5fce8c5
--- /dev/null
+++ b/src/com/gitblit/client/EditUserDialog.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPasswordField;
+import javax.swing.JTextField;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.IStoredSettings;
+import com.gitblit.Keys;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+
+public class EditUserDialog extends JDialog {
+
+	private static final long serialVersionUID = 1L;
+
+	private final UserModel user;
+
+	private final IStoredSettings settings;
+
+	private boolean canceled = true;
+
+	private JTextField usernameField;
+
+	private JPasswordField passwordField;
+
+	private JPasswordField confirmPasswordField;
+
+	private JCheckBox canAdminCheckbox;
+
+	private JCheckBox notFederatedCheckbox;
+
+	private JPalette<String> repositoryPalette;
+
+	public EditUserDialog(IStoredSettings settings) {
+		this(new UserModel(""), settings);
+		setTitle("Create User");
+	}
+
+	public EditUserDialog(UserModel anUser, IStoredSettings settings) {
+		super();
+		this.user = new UserModel("");
+		this.settings = settings;
+		initialize(anUser);
+		setModal(true);
+		setTitle("Edit User: " + anUser.username);
+		setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage());
+	}
+
+	private void initialize(UserModel anUser) {
+		usernameField = new JTextField(anUser.username == null ? "" : anUser.username, 25);
+		passwordField = new JPasswordField(anUser.password == null ? "" : anUser.password, 25);
+		confirmPasswordField = new JPasswordField(anUser.password == null ? "" : anUser.password,
+				25);
+		canAdminCheckbox = new JCheckBox("can administer Gitblit server", anUser.canAdmin);
+		notFederatedCheckbox = new JCheckBox(
+				"block federated Gitblit instances from pulling this account",
+				anUser.excludeFromFederation);
+
+		JPanel fieldsPanel = new JPanel(new GridLayout(0, 1));
+		fieldsPanel.add(newFieldPanel("username", usernameField));
+		fieldsPanel.add(newFieldPanel("password", passwordField));
+		fieldsPanel.add(newFieldPanel("confirm password", confirmPasswordField));
+		fieldsPanel.add(newFieldPanel("can admin", canAdminCheckbox));
+		fieldsPanel.add(newFieldPanel("exclude from federation", notFederatedCheckbox));
+
+		repositoryPalette = new JPalette<String>();
+		JPanel panel = new JPanel(new BorderLayout());
+		panel.add(fieldsPanel, BorderLayout.NORTH);
+		panel.add(newFieldPanel("restricted repositories", repositoryPalette), BorderLayout.CENTER);
+
+		JButton createButton = new JButton("Save");
+		createButton.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent event) {
+				if (validateFields()) {
+					canceled = false;
+					setVisible(false);
+				}
+			}
+		});
+
+		JButton cancelButton = new JButton("Cancel");
+		cancelButton.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent event) {
+				canceled = true;
+				setVisible(false);
+			}
+		});
+
+		JPanel controls = new JPanel();
+		controls.add(cancelButton);
+		controls.add(createButton);
+
+		final Insets _insets = new Insets(5, 5, 5, 5);
+		JPanel centerPanel = new JPanel(new BorderLayout(5, 5)) {
+
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			public Insets getInsets() {
+				return _insets;
+			}
+		};
+		centerPanel.add(panel, BorderLayout.CENTER);
+		centerPanel.add(controls, BorderLayout.SOUTH);
+
+		getContentPane().setLayout(new BorderLayout(5, 5));
+		getContentPane().add(centerPanel, BorderLayout.CENTER);
+		pack();
+		setLocationRelativeTo(null);
+	}
+
+	private JPanel newFieldPanel(String label, JComponent comp) {
+		JLabel fieldLabel = new JLabel(label);
+		fieldLabel.setFont(fieldLabel.getFont().deriveFont(Font.BOLD));
+		fieldLabel.setPreferredSize(new Dimension(150, 20));
+		JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
+		panel.add(fieldLabel);
+		panel.add(comp);
+		return panel;
+	}
+
+	private boolean validateFields() {
+		String uname = usernameField.getText();
+		if (StringUtils.isEmpty(uname)) {
+			showValidationError("Please enter a username!");
+			return false;
+		}
+
+		// TODO verify username uniqueness on create
+		
+		// if (isCreate) {
+		// UserModel model = GitBlit.self().getUserModel(username);
+		// if (model != null) {
+		// error(MessageFormat.format("Username ''{0}'' is unavailable.",
+		// username));
+		// return;
+		// }
+		// }
+
+		int minLength = settings.getInteger(Keys.realm.minPasswordLength, 5);
+		if (minLength < 4) {
+			minLength = 4;
+		}
+		char[] pw = passwordField.getPassword();
+		if (pw == null || pw.length < minLength) {
+			showValidationError(MessageFormat.format(
+					"Password is too short. Minimum length is {0} characters.", minLength));
+			return false;
+		}
+		char[] cpw = confirmPasswordField.getPassword();
+		if (cpw == null || cpw.length != pw.length) {
+			showValidationError("Please confirm the password!");
+			return false;
+		}
+		if (!Arrays.equals(pw, cpw)) {
+			showValidationError("Passwords do not match!");
+			return false;
+		}
+		user.username = uname;
+		String type = settings.getString(Keys.realm.passwordStorage, "md5");
+		if (type.equalsIgnoreCase("md5")) {
+			// store MD5 digest of password
+			user.password = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(pw));
+		} else {
+			user.password = new String(pw);
+		}
+		user.canAdmin = canAdminCheckbox.isSelected();
+		user.excludeFromFederation = notFederatedCheckbox.isSelected();
+
+		user.repositories.clear();
+		user.repositories.addAll(repositoryPalette.getSelections());
+		return true;
+	}
+
+	private void showValidationError(String message) {
+		JOptionPane.showMessageDialog(EditUserDialog.this, message, "Validation Error",
+				JOptionPane.ERROR_MESSAGE);
+	}
+
+	public void setRepositories(List<RepositoryModel> repositories, List<String> selected) {
+		List<String> restricted = new ArrayList<String>();
+		for (RepositoryModel repo : repositories) {
+			if (repo.accessRestriction.exceeds(AccessRestrictionType.NONE)) {
+				restricted.add(repo.name);
+			}
+		}
+		StringUtils.sortRepositorynames(restricted);
+		if (selected != null) {
+			StringUtils.sortRepositorynames(selected);
+		}
+		repositoryPalette.setObjects(restricted, selected);
+	}
+
+	public UserModel getUser() {
+		if (canceled) {
+			return null;
+		}
+		return user;
+	}
+}
diff --git a/src/com/gitblit/client/GitblitClient.java b/src/com/gitblit/client/GitblitClient.java
index 51d8e7e..65a8c36 100644
--- a/src/com/gitblit/client/GitblitClient.java
+++ b/src/com/gitblit/client/GitblitClient.java
@@ -148,13 +148,13 @@
 		}
 		reg = new GitblitRegistration(nameField.getText(), url, accountField.getText(),
 				passwordField.getPassword());
-		login(reg);
+		boolean success = login(reg);
 		registrations.add(0, reg);
 		rebuildRecentMenu();
-		return true;
+		return success;
 	}
 
-	private void login(GitblitRegistration reg) {
+	private boolean login(GitblitRegistration reg) {
 		try {
 			GitblitPanel panel = new GitblitPanel(reg);
 			panel.login();
@@ -163,10 +163,12 @@
 			serverTabs.setSelectedIndex(idx);
 			serverTabs.setTabComponentAt(idx, new ClosableTabComponent(reg.name, null, serverTabs,
 					panel));
+			return true;
 		} catch (IOException e) {
 			JOptionPane.showMessageDialog(GitblitClient.this, e.getMessage(), "Error",
 					JOptionPane.ERROR_MESSAGE);
 		}
+		return false;
 	}
 
 	private void rebuildRecentMenu() {
diff --git a/src/com/gitblit/client/GitblitClientLauncher.java b/src/com/gitblit/client/GitblitClientLauncher.java
index 19e9efd..24832d1 100644
--- a/src/com/gitblit/client/GitblitClientLauncher.java
+++ b/src/com/gitblit/client/GitblitClientLauncher.java
@@ -25,6 +25,7 @@
 import java.util.Collections;
 import java.util.List;
 
+import com.gitblit.Constants;
 import com.gitblit.Launcher;
 import com.gitblit.build.Build;
 import com.gitblit.build.Build.DownloadListener;
@@ -82,6 +83,8 @@
 					if (g != null) {
 						// Splash is 320x120
 						FontMetrics fm = g.getFontMetrics();
+						
+						// paint startup status
 						g.setColor(Color.darkGray);
 						int h = fm.getHeight() + fm.getMaxDescent();
 						int x = 5;
@@ -93,6 +96,11 @@
 						g.setColor(Color.WHITE);
 						int xw = fm.stringWidth(string);
 						g.drawString(string, x + ((w - xw) / 2), y - 5);
+						
+						// paint version
+						String ver = "v" + Constants.VERSION;
+						int vw = g.getFontMetrics().stringWidth(ver);
+						g.drawString(ver, 320 - vw - 5, 34);
 						g.dispose();
 						splash.update();
 					}
diff --git a/src/com/gitblit/client/GitblitPanel.java b/src/com/gitblit/client/GitblitPanel.java
index 5482593..d29f2a9 100644
--- a/src/com/gitblit/client/GitblitPanel.java
+++ b/src/com/gitblit/client/GitblitPanel.java
@@ -20,6 +20,7 @@
 import java.awt.Component;
 import java.awt.Desktop;
 import java.awt.Dimension;
+import java.awt.GridLayout;
 import java.awt.Insets;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
@@ -27,6 +28,7 @@
 import java.net.URI;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
@@ -34,6 +36,7 @@
 import javax.swing.JButton;
 import javax.swing.JLabel;
 import javax.swing.JList;
+import javax.swing.JOptionPane;
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
 import javax.swing.JTabbedPane;
@@ -41,6 +44,7 @@
 import javax.swing.JTextField;
 import javax.swing.RowFilter;
 import javax.swing.SwingConstants;
+import javax.swing.SwingWorker;
 import javax.swing.event.ListSelectionEvent;
 import javax.swing.event.ListSelectionListener;
 import javax.swing.table.DefaultTableCellRenderer;
@@ -49,7 +53,11 @@
 import javax.swing.table.TableColumn;
 import javax.swing.table.TableRowSorter;
 
+import com.gitblit.Constants.RpcRequest;
 import com.gitblit.GitBlitException.ForbiddenException;
+import com.gitblit.GitBlitException.UnauthorizedException;
+import com.gitblit.IStoredSettings;
+import com.gitblit.Keys;
 import com.gitblit.client.ClosableTabComponent.CloseTabListener;
 import com.gitblit.models.FederationModel;
 import com.gitblit.models.RepositoryModel;
@@ -72,13 +80,17 @@
 
 	private final Insets insets = new Insets(margin, margin, margin, margin);
 
-	private String url;
+	private final String url;
 
-	private String account;
+	private final String account;
 
-	private char[] password;
+	private final char[] password;
 
-	private boolean isAdmin;
+	private volatile boolean isAdmin;
+
+	private volatile List<UserModel> allUsers;
+
+	private volatile IStoredSettings settings;
 
 	private JTabbedPane tabs;
 
@@ -104,6 +116,8 @@
 
 	private TableRowSorter<RepositoriesModel> defaultSorter;
 
+	private List<RepositoryModel> allRepositories;
+
 	public GitblitPanel(GitblitRegistration reg) {
 		this(reg.url, reg.account, reg.password);
 	}
@@ -128,10 +142,25 @@
 			}
 		});
 
+		JButton refreshRepositories = new JButton("Refresh");
+		refreshRepositories.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				try {
+					refreshRepositoriesTable();
+				} catch (ForbiddenException x) {
+					explainForbidden(RpcRequest.LIST_REPOSITORIES);
+				} catch (UnauthorizedException x) {
+					explainUnauthorized(RpcRequest.LIST_REPOSITORIES);
+				} catch (Throwable t) {
+					showException(t);
+				}
+			}
+		});
+
 		createRepository = new JButton("Create");
 		createRepository.addActionListener(new ActionListener() {
 			public void actionPerformed(ActionEvent e) {
-				System.out.println("TODO Create Repository");
+				createRepository();
 			}
 		});
 
@@ -139,9 +168,7 @@
 		editRepository.setEnabled(false);
 		editRepository.addActionListener(new ActionListener() {
 			public void actionPerformed(ActionEvent e) {
-				for (RepositoryModel model : getSelectedRepositories()) {
-					System.out.println("TODO Edit " + model);
-				}
+				editRepository(getSelectedRepositories().get(0));
 			}
 		});
 
@@ -149,9 +176,7 @@
 		delRepository.setEnabled(false);
 		delRepository.addActionListener(new ActionListener() {
 			public void actionPerformed(ActionEvent e) {
-				for (RepositoryModel model : getSelectedRepositories()) {
-					System.out.println("TODO Delete " + model);
-				}
+				deleteRepositories(getSelectedRepositories());
 			}
 		});
 
@@ -165,7 +190,7 @@
 			}
 		});
 
-		nameRenderer = new NameRenderer(Color.gray, new Color(0x00, 0x69, 0xD6));
+		nameRenderer = new NameRenderer();
 		typeRenderer = new TypeRenderer();
 
 		sizeRenderer = new DefaultTableCellRenderer();
@@ -246,10 +271,25 @@
 		repositoriesPanel.add(tablePanel, BorderLayout.CENTER);
 		repositoriesPanel.add(repositoryControls, BorderLayout.SOUTH);
 
+		JButton refreshUsers = new JButton("Refresh");
+		refreshUsers.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				try {
+					refreshUsersTable();
+				} catch (ForbiddenException x) {
+					explainForbidden(RpcRequest.LIST_USERS);
+				} catch (UnauthorizedException x) {
+					explainUnauthorized(RpcRequest.LIST_USERS);
+				} catch (Throwable t) {
+					showException(t);
+				}
+			}
+		});
+
 		JButton createUser = new JButton("Create");
 		createUser.addActionListener(new ActionListener() {
 			public void actionPerformed(ActionEvent e) {
-				System.out.println("TODO Create User");
+				createUser();
 			}
 		});
 
@@ -257,9 +297,7 @@
 		editUser.setEnabled(false);
 		editUser.addActionListener(new ActionListener() {
 			public void actionPerformed(ActionEvent e) {
-				for (UserModel user : getSelectedUsers()) {
-					System.out.println("TODO Edit " + user);
-				}
+				editUser(getSelectedUsers().get(0));
 			}
 		});
 
@@ -267,9 +305,7 @@
 		delUser.setEnabled(false);
 		delUser.addActionListener(new ActionListener() {
 			public void actionPerformed(ActionEvent e) {
-				for (UserModel user : getSelectedUsers()) {
-					System.out.println("TODO Delete " + user);
-				}
+				deleteUsers(getSelectedUsers());
 			}
 		});
 
@@ -288,7 +324,8 @@
 			}
 		});
 
-		JPanel userControls = new JPanel();
+		JPanel userControls = new JPanel(new GridLayout(0, 2));
+		userControls.add(refreshUsers);
 		userControls.add(createUser);
 		userControls.add(editUser);
 		userControls.add(delUser);
@@ -327,6 +364,7 @@
 
 		try {
 			refreshUsersTable();
+			refreshSettings();
 			isAdmin = true;
 			refreshFederationPanel();
 		} catch (ForbiddenException e) {
@@ -348,8 +386,10 @@
 	private void refreshRepositoriesTable() throws IOException {
 		Map<String, RepositoryModel> repositories = RpcUtils
 				.getRepositories(url, account, password);
+		allRepositories = new ArrayList<RepositoryModel>(repositories.values());
+		Collections.sort(allRepositories);
 		repositoriesModel.list.clear();
-		repositoriesModel.list.addAll(repositories.values());
+		repositoriesModel.list.addAll(allRepositories);
 		repositoriesModel.fireTableDataChanged();
 		packColumns(repositoriesTable, 2);
 	}
@@ -360,8 +400,12 @@
 	}
 
 	private void refreshUsersTable() throws IOException {
-		List<UserModel> users = RpcUtils.getUsers(url, account, password);
-		usersList.setListData(users.toArray());
+		allUsers = RpcUtils.getUsers(url, account, password);
+		usersList.setListData(allUsers.toArray());
+	}
+
+	private void refreshSettings() throws IOException {
+		settings = RpcUtils.getSettings(url, account, password);
 	}
 
 	private void refreshFederationPanel() throws IOException {
@@ -465,4 +509,313 @@
 	@Override
 	public void closeTab(Component c) {
 	}
+
+	/**
+	 * Displays the create repository dialog and fires a SwingWorker to update
+	 * the server, if appropriate.
+	 * 
+	 */
+	protected void createRepository() {
+		EditRepositoryDialog dialog = new EditRepositoryDialog(allUsers);
+		dialog.setVisible(true);
+		final RepositoryModel newRepository = dialog.getRepository();
+		if (newRepository == null) {
+			return;
+		}
+
+		final RpcRequest request = RpcRequest.CREATE_REPOSITORY;
+		SwingWorker<Boolean, Void> worker = new SwingWorker<Boolean, Void>() {
+
+			@Override
+			protected Boolean doInBackground() throws IOException {
+				return RpcUtils.createRepository(newRepository, url, account, password);
+			}
+
+			@Override
+			protected void done() {
+				try {
+					boolean success = get();
+					if (success) {
+						refreshRepositoriesTable();
+					} else {
+						String msg = MessageFormat.format(
+								"Failed to execute request \"{0}\" for repository \"{1}\".",
+								request.name(), newRepository.name);
+						JOptionPane.showMessageDialog(GitblitPanel.this, msg, "Error!",
+								JOptionPane.ERROR_MESSAGE);
+					}
+				} catch (ForbiddenException e) {
+					explainForbidden(request);
+				} catch (UnauthorizedException e) {
+					explainUnauthorized(request);
+				} catch (Throwable t) {
+					showException(t);
+				}
+			}
+		};
+		worker.execute();
+	}
+
+	/**
+	 * Displays the edit repository dialog and fires a SwingWorker to update the
+	 * server, if appropriate.
+	 * 
+	 * @param repository
+	 */
+	protected void editRepository(final RepositoryModel repository) {
+		EditRepositoryDialog dialog = new EditRepositoryDialog(repository, allUsers);
+		List<String> usernames = new ArrayList<String>();
+		for (UserModel user : this.allUsers) {
+			usernames.add(user.username);
+		}
+		Collections.sort(usernames);
+		dialog.setUsers(usernames, null);
+		dialog.setFederationSets(settings.getStrings(Keys.federation.sets),
+				repository.federationSets);
+		dialog.setVisible(true);
+		final RepositoryModel revisedRepository = dialog.getRepository();
+		if (revisedRepository == null) {
+			return;
+		}
+
+		final RpcRequest request = RpcRequest.EDIT_REPOSITORY;
+		SwingWorker<Boolean, Void> worker = new SwingWorker<Boolean, Void>() {
+
+			@Override
+			protected Boolean doInBackground() throws IOException {
+				return RpcUtils.updateRepository(repository.name, revisedRepository, url, account,
+						password);
+			}
+
+			@Override
+			protected void done() {
+				try {
+					boolean success = get();
+					if (success) {
+						refreshRepositoriesTable();
+					} else {
+						String msg = MessageFormat.format(
+								"Failed to execute request \"{0}\" for repository \"{1}\".",
+								request.name(), repository.name);
+						JOptionPane.showMessageDialog(GitblitPanel.this, msg, "Error!",
+								JOptionPane.ERROR_MESSAGE);
+					}
+				} catch (ForbiddenException e) {
+					explainForbidden(request);
+				} catch (UnauthorizedException e) {
+					explainUnauthorized(request);
+				} catch (Throwable t) {
+					showException(t);
+				}
+			}
+		};
+		worker.execute();
+	}
+
+	protected void deleteRepositories(final List<RepositoryModel> repositories) {
+		if (repositories == null || repositories.size() == 0) {
+			return;
+		}
+		StringBuilder message = new StringBuilder("Delete the following repositories?\n\n");
+		for (RepositoryModel repository : repositories) {
+			message.append(repository.name).append("\n");
+		}
+		int result = JOptionPane.showConfirmDialog(GitblitPanel.this, message.toString(),
+				"Delete Repositories?", JOptionPane.YES_NO_OPTION);
+		if (result == JOptionPane.YES_OPTION) {
+			final RpcRequest request = RpcRequest.DELETE_REPOSITORY;
+			SwingWorker<Boolean, Void> worker = new SwingWorker<Boolean, Void>() {
+				@Override
+				protected Boolean doInBackground() throws Exception {
+					boolean success = true;
+					for (RepositoryModel repository : repositories) {
+						success &= RpcUtils.deleteRepository(repository, url, account, password);
+					}
+					return success;
+				}
+
+				@Override
+				protected void done() {
+					try {
+						boolean success = get();
+						if (success) {
+							refreshRepositoriesTable();
+						} else {
+							String msg = "Failed to delete specified repositories!";
+							JOptionPane.showMessageDialog(GitblitPanel.this, msg, "Error!",
+									JOptionPane.ERROR_MESSAGE);
+						}
+					} catch (ForbiddenException e) {
+						explainForbidden(request);
+					} catch (UnauthorizedException e) {
+						explainUnauthorized(request);
+					} catch (Throwable t) {
+						showException(t);
+					}
+				}
+			};
+			worker.execute();
+		}
+	}
+
+	/**
+	 * Displays the create user dialog and fires a SwingWorker to update the
+	 * server, if appropriate.
+	 * 
+	 */
+	protected void createUser() {
+		EditUserDialog dialog = new EditUserDialog(settings);
+		dialog.setRepositories(allRepositories, null);
+		dialog.setVisible(true);
+		final UserModel newUser = dialog.getUser();
+		if (newUser == null) {
+			return;
+		}
+
+		final RpcRequest request = RpcRequest.CREATE_USER;
+		SwingWorker<Boolean, Void> worker = new SwingWorker<Boolean, Void>() {
+
+			@Override
+			protected Boolean doInBackground() throws IOException {
+				return RpcUtils.createUser(newUser, url, account, password);
+			}
+
+			@Override
+			protected void done() {
+				try {
+					boolean success = get();
+					if (success) {
+						refreshUsersTable();
+					} else {
+						String msg = MessageFormat.format(
+								"Failed to execute request \"{0}\" for user \"{1}\".",
+								request.name(), newUser.username);
+						JOptionPane.showMessageDialog(GitblitPanel.this, msg, "Error!",
+								JOptionPane.ERROR_MESSAGE);
+					}
+				} catch (ForbiddenException e) {
+					explainForbidden(request);
+				} catch (UnauthorizedException e) {
+					explainUnauthorized(request);
+				} catch (Throwable t) {
+					showException(t);
+				}
+			}
+		};
+		worker.execute();
+	}
+
+	/**
+	 * Displays the edit user dialog and fires a SwingWorker to update the
+	 * server, if appropriate.
+	 * 
+	 * @param user
+	 */
+	protected void editUser(final UserModel user) {
+		EditUserDialog dialog = new EditUserDialog(user, settings);
+		dialog.setRepositories(allRepositories, new ArrayList<String>(user.repositories));
+		dialog.setVisible(true);
+		final UserModel revisedUser = dialog.getUser();
+		if (revisedUser == null) {
+			return;
+		}
+
+		final RpcRequest request = RpcRequest.EDIT_USER;
+		SwingWorker<Boolean, Void> worker = new SwingWorker<Boolean, Void>() {
+
+			@Override
+			protected Boolean doInBackground() throws IOException {
+				return RpcUtils.updateUser(user.username, revisedUser, url, account, password);
+			}
+
+			@Override
+			protected void done() {
+				try {
+					boolean success = get();
+					if (success) {
+						refreshUsersTable();
+					} else {
+						String msg = MessageFormat.format(
+								"Failed to execute request \"{0}\" for user \"{1}\".",
+								request.name(), user.username);
+						JOptionPane.showMessageDialog(GitblitPanel.this, msg, "Error!",
+								JOptionPane.ERROR_MESSAGE);
+					}
+				} catch (ForbiddenException e) {
+					explainForbidden(request);
+				} catch (UnauthorizedException e) {
+					explainUnauthorized(request);
+				} catch (Throwable t) {
+					showException(t);
+				}
+			}
+		};
+		worker.execute();
+	}
+
+	protected void deleteUsers(final List<UserModel> users) {
+		if (users == null || users.size() == 0) {
+			return;
+		}
+		StringBuilder message = new StringBuilder("Delete the following users?\n\n");
+		for (UserModel user : users) {
+			message.append(user.username).append("\n");
+		}
+		int result = JOptionPane.showConfirmDialog(GitblitPanel.this, message.toString(),
+				"Delete Users?", JOptionPane.YES_NO_OPTION);
+		if (result == JOptionPane.YES_OPTION) {
+			final RpcRequest request = RpcRequest.DELETE_USER;
+			SwingWorker<Boolean, Void> worker = new SwingWorker<Boolean, Void>() {
+				@Override
+				protected Boolean doInBackground() throws Exception {
+					boolean success = true;
+					for (UserModel user : users) {
+						success &= RpcUtils.deleteUser(user, url, account, password);
+					}
+					return success;
+				}
+
+				@Override
+				protected void done() {
+					try {
+						boolean success = get();
+						if (success) {
+							refreshUsersTable();
+						} else {
+							String msg = "Failed to delete specified users!";
+							JOptionPane.showMessageDialog(GitblitPanel.this, msg, "Error!",
+									JOptionPane.ERROR_MESSAGE);
+						}
+					} catch (ForbiddenException e) {
+						explainForbidden(request);
+					} catch (UnauthorizedException e) {
+						explainUnauthorized(request);
+					} catch (Throwable t) {
+						showException(t);
+					}
+				}
+			};
+			worker.execute();
+		}
+	}
+
+	private void explainForbidden(RpcRequest request) {
+		String msg = MessageFormat.format(
+				"The request \"{0}\" has been forbidden by the Gitblit server @ {1}.",
+				request.name(), url);
+		JOptionPane.showMessageDialog(GitblitPanel.this, msg, "Forbidden",
+				JOptionPane.ERROR_MESSAGE);
+	}
+
+	private void explainUnauthorized(RpcRequest request) {
+		String msg = MessageFormat.format(
+				"The account \"{0}\" is not authorized to execute the request \"{1}\".", account,
+				request.name());
+		JOptionPane.showMessageDialog(GitblitPanel.this, msg, "Unauthorized",
+				JOptionPane.ERROR_MESSAGE);
+	}
+
+	private void showException(Throwable t) {
+		// TODO show the unexpected exception
+	}
 }
diff --git a/src/com/gitblit/client/JPalette.java b/src/com/gitblit/client/JPalette.java
new file mode 100644
index 0000000..19456e1
--- /dev/null
+++ b/src/com/gitblit/client/JPalette.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.GridBagLayout;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.table.AbstractTableModel;
+
+public class JPalette<T> extends JPanel {
+
+	private static final long serialVersionUID = 1L;
+	private PaletteModel<T> availableModel;
+	private PaletteModel<T> selectedModel;
+
+	public JPalette() {
+		super(new BorderLayout(5, 5));
+
+		availableModel = new PaletteModel<T>();
+		selectedModel = new PaletteModel<T>();
+
+		final JTable available = new JTable(availableModel);
+		final JTable selected = new JTable(selectedModel);
+
+		JButton add = new JButton("->");
+		add.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent event) {
+				List<T> move = new ArrayList<T>();
+				if (available.getSelectedRowCount() <= 0) {
+					return;
+				}
+				for (int row : available.getSelectedRows()) {
+					int modelIndex = available.convertRowIndexToModel(row);
+					T item = (T) availableModel.list.get(modelIndex);
+					move.add(item);
+				}
+				availableModel.list.removeAll(move);
+				selectedModel.list.addAll(move);
+				availableModel.fireTableDataChanged();
+				selectedModel.fireTableDataChanged();
+			}
+		});
+		JButton subtract = new JButton("<-");
+		subtract.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent event) {
+				List<T> move = new ArrayList<T>();
+				if (selected.getSelectedRowCount() <= 0) {
+					return;
+				}
+				for (int row : selected.getSelectedRows()) {
+					int modelIndex = selected.convertRowIndexToModel(row);
+					T item = (T) selectedModel.list.get(modelIndex);
+					move.add(item);
+				}
+				selectedModel.list.removeAll(move);
+				availableModel.list.addAll(move);
+
+				selectedModel.fireTableDataChanged();
+				availableModel.fireTableDataChanged();
+			}
+		});
+
+		JPanel controls = new JPanel(new GridLayout(0, 1, 0, 5));
+		controls.add(add);
+		controls.add(subtract);
+
+		JPanel center = new JPanel(new GridBagLayout());
+		center.add(controls);
+
+		add(newListPanel("Available", available), BorderLayout.WEST);
+		add(center, BorderLayout.CENTER);
+		add(newListPanel("Selected", selected), BorderLayout.EAST);
+	}
+
+	private JPanel newListPanel(String label, JTable table) {
+		NameRenderer nameRenderer = new NameRenderer();
+		table.setCellSelectionEnabled(false);
+		table.setRowSelectionAllowed(true);
+		table.setRowHeight(nameRenderer.getFont().getSize() + 8);
+		table.getTableHeader().setReorderingAllowed(false);
+		table.setGridColor(new Color(0xd9d9d9));
+		table.setBackground(Color.white);
+		table.getColumn(table.getColumnName(0)).setCellRenderer(nameRenderer);
+
+		JScrollPane jsp = new JScrollPane(table);
+		jsp.setPreferredSize(new Dimension(225, 175));
+		JPanel panel = new JPanel(new BorderLayout());
+		panel.add(new JLabel(label), BorderLayout.NORTH);
+		panel.add(jsp, BorderLayout.CENTER);
+		return panel;
+	}
+
+	public void setObjects(List<T> all, List<T> selected) {
+		List<T> available = new ArrayList<T>(all);
+		if (selected != null) {
+			available.removeAll(selected);
+		}
+		availableModel.list.clear();
+		availableModel.list.addAll(available);
+		availableModel.fireTableDataChanged();
+
+		if (selected != null) {
+			selectedModel.list.clear();
+			selectedModel.list.addAll(selected);
+			selectedModel.fireTableDataChanged();
+		}
+	}
+
+	public List<T> getSelections() {
+		return new ArrayList<T>(selectedModel.list);
+	}
+
+	public class PaletteModel<K> extends AbstractTableModel {
+
+		private static final long serialVersionUID = 1L;
+
+		List<K> list;
+
+		public PaletteModel() {
+			this(new ArrayList<K>());
+		}
+
+		public PaletteModel(List<K> list) {
+			this.list = new ArrayList<K>(list);
+		}
+
+		@Override
+		public int getRowCount() {
+			return list.size();
+		}
+
+		@Override
+		public int getColumnCount() {
+			return 1;
+		}
+
+		@Override
+		public String getColumnName(int column) {
+			return "Name";
+		}
+
+		public Class<?> getColumnClass(int columnIndex) {
+			return String.class;
+		}
+
+		@Override
+		public Object getValueAt(int rowIndex, int columnIndex) {
+			K o = list.get(rowIndex);
+			return o.toString();
+		}
+	}
+}
diff --git a/src/com/gitblit/client/NameRenderer.java b/src/com/gitblit/client/NameRenderer.java
index 41393fb..5b1a173 100644
--- a/src/com/gitblit/client/NameRenderer.java
+++ b/src/com/gitblit/client/NameRenderer.java
@@ -34,6 +34,10 @@
 
 	final String groupSpan;
 
+	public NameRenderer() {
+		this(Color.gray, new Color(0x00, 0x69, 0xD6));
+	}
+
 	public NameRenderer(Color group, Color repo) {
 		groupSpan = "<span style='color:" + getHexColor(group) + "'>";
 		setForeground(repo);
diff --git a/src/com/gitblit/utils/JsonUtils.java b/src/com/gitblit/utils/JsonUtils.java
index 3834c8e..fee7990 100644
--- a/src/com/gitblit/utils/JsonUtils.java
+++ b/src/com/gitblit/utils/JsonUtils.java
@@ -157,6 +157,25 @@
 		}
 		return gson().fromJson(json, type);
 	}
+	
+	/**
+	 * Reads a gson object from the specified url.
+	 * 
+	 * @param url
+	 * @param clazz
+	 * @param username
+	 * @param password
+	 * @return the deserialized object
+	 * @throws {@link IOException}
+	 */
+	public static <X> X retrieveJson(String url, Class<X> clazz, String username, char[] password)
+			throws IOException {
+		String json = retrieveJsonString(url, username, password);
+		if (StringUtils.isEmpty(json)) {
+			return null;
+		}
+		return gson().fromJson(json, clazz);
+	}
 
 	/**
 	 * Retrieves a JSON message.
diff --git a/src/com/gitblit/utils/RpcUtils.java b/src/com/gitblit/utils/RpcUtils.java
index 715ecb5..eb28c0f 100644
--- a/src/com/gitblit/utils/RpcUtils.java
+++ b/src/com/gitblit/utils/RpcUtils.java
@@ -21,9 +21,11 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.Properties;
 
 import com.gitblit.Constants;
 import com.gitblit.Constants.RpcRequest;
+import com.gitblit.IStoredSettings;
 import com.gitblit.models.FederationModel;
 import com.gitblit.models.FederationProposal;
 import com.gitblit.models.FederationSet;
@@ -294,7 +296,7 @@
 		List<FederationModel> list = new ArrayList<FederationModel>(registrations);
 		return list;
 	}
-	
+
 	/**
 	 * Retrieves the list of federation proposals.
 	 * 
@@ -304,15 +306,15 @@
 	 * @return a collection of FederationProposal objects
 	 * @throws IOException
 	 */
-	public static List<FederationProposal> getFederationProposals(String serverUrl,
-			String account, char[] password) throws IOException {
+	public static List<FederationProposal> getFederationProposals(String serverUrl, String account,
+			char[] password) throws IOException {
 		String url = asLink(serverUrl, RpcRequest.LIST_FEDERATION_PROPOSALS);
 		Collection<FederationProposal> proposals = JsonUtils.retrieveJson(url, PROPOSALS_TYPE,
 				account, password);
 		List<FederationProposal> list = new ArrayList<FederationProposal>(proposals);
 		return list;
 	}
-	
+
 	/**
 	 * Retrieves the list of federation repository sets.
 	 * 
@@ -322,13 +324,29 @@
 	 * @return a collection of FederationSet objects
 	 * @throws IOException
 	 */
-	public static List<FederationSet> getFederationSets(String serverUrl,
-			String account, char[] password) throws IOException {
+	public static List<FederationSet> getFederationSets(String serverUrl, String account,
+			char[] password) throws IOException {
 		String url = asLink(serverUrl, RpcRequest.LIST_FEDERATION_SETS);
-		Collection<FederationSet> sets = JsonUtils.retrieveJson(url, SETS_TYPE,
-				account, password);
+		Collection<FederationSet> sets = JsonUtils.retrieveJson(url, SETS_TYPE, account, password);
 		List<FederationSet> list = new ArrayList<FederationSet>(sets);
 		return list;
+	}
+
+	/**
+	 * Retrieves the settings of the Gitblit server.
+	 * 
+	 * @param serverUrl
+	 * @param account
+	 * @param password
+	 * @return an IStoredSettings object
+	 * @throws IOException
+	 */
+	public static IStoredSettings getSettings(String serverUrl, String account, char[] password)
+			throws IOException {
+		String url = asLink(serverUrl, RpcRequest.LIST_SETTINGS);
+		Properties props = JsonUtils.retrieveJson(url, Properties.class, account, password);
+		RpcSettings settings = new RpcSettings(props);
+		return settings;
 	}
 
 	/**
@@ -351,4 +369,31 @@
 		int resultCode = JsonUtils.sendJsonString(url, json, account, password);
 		return resultCode == 200;
 	}
+	
+	/**
+	 * Settings implementation that wraps a retrieved properties instance. This
+	 * class is used for RPC communication.
+	 * 
+	 * @author James Moger
+	 * 
+	 */
+	private static class RpcSettings extends IStoredSettings {
+		
+		private final Properties properties = new Properties();
+
+		public RpcSettings(Properties props) {
+			super(RpcSettings.class);
+			properties.putAll(props);
+		}
+
+		@Override
+		protected Properties read() {
+			return properties;
+		}
+
+		@Override
+		public String toString() {
+			return "RpcSettings";
+		}
+	}
 }
diff --git a/src/com/gitblit/wicket/GitBlitWebApp.properties b/src/com/gitblit/wicket/GitBlitWebApp.properties
index 2aa5107..30fac60 100644
--- a/src/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/src/com/gitblit/wicket/GitBlitWebApp.properties
@@ -92,7 +92,7 @@
 gb.isFrozenDescription = deny push operations
 gb.zip = zip
 gb.showReadme = show readme
-gb.showReadmeDescription = show a \"readme\" markdown file on the summary page
+gb.showReadmeDescription = show a \"readme\" Markdown file on the summary page
 gb.nameDescription = use '/' to group repositories.  e.g. libraries/mycoollib.git
 gb.ownerDescription = the owner may edit repository settings
 gb.blob = blob
@@ -106,7 +106,7 @@
 gb.federateThis = federate this repository
 gb.federateOrigin = federate the origin
 gb.excludeFromFederation = exclude from federation
-gb.excludeFromFederationDescription = block federated Gitblit instances from pulling this object
+gb.excludeFromFederationDescription = block federated Gitblit instances from pulling this account
 gb.tokens = federation tokens
 gb.tokenAllDescription = all repositories, users, & settings
 gb.tokenUnrDescription = all repositories & users
diff --git a/tests/com/gitblit/tests/RpcTests.java b/tests/com/gitblit/tests/RpcTests.java
index a20b918..450b597 100644
--- a/tests/com/gitblit/tests/RpcTests.java
+++ b/tests/com/gitblit/tests/RpcTests.java
@@ -23,6 +23,7 @@
 
 import com.gitblit.Constants.AccessRestrictionType;
 import com.gitblit.GitBlitException.UnauthorizedException;
+import com.gitblit.IStoredSettings;
 import com.gitblit.models.FederationModel;
 import com.gitblit.models.FederationProposal;
 import com.gitblit.models.FederationSet;
@@ -203,4 +204,9 @@
 		List<FederationSet> sets = RpcUtils.getFederationSets(url, account, password.toCharArray());
 		assertTrue("No federation sets were retrieved!", sets.size() > 0);
 	}
+
+	public void testSettings() throws Exception {
+		IStoredSettings settings = RpcUtils.getSettings(url, account, password.toCharArray());
+		assertTrue("No settings were retrieved!", settings.getAllKeys(null).size() > 0);
+	}
 }

--
Gitblit v1.9.1