From b75734f0600c333d70a3659af82be54caf3cfd3e Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Fri, 21 Oct 2011 17:34:12 -0400
Subject: [PATCH] Revised settings RPC to be Map<String, SettingModel>.

---
 docs/02_rpc.mkd                                 |   34 ++
 src/com/gitblit/client/SettingsModel.java       |   24 +
 src/com/gitblit/client/GitblitModel.java        |   17 +
 src/com/gitblit/client/GitblitPanel.java        |   13 +
 src/com/gitblit/client/SettingCellRenderer.java |   67 +++++
 src/com/gitblit/client/SettingPanel.java        |  114 +++++++++
 src/com/gitblit/utils/RpcUtils.java             |   60 ++--
 src/com/gitblit/models/ServerStatus.java        |   69 +++++
 src/com/gitblit/client/EditUserDialog.java      |   13 
 src/com/gitblit/RpcServlet.java                 |   14 
 src/com/gitblit/GitBlit.java                    |  103 ++++++++
 src/com/gitblit/models/SettingModel.java        |  144 ++++++++++++
 tests/com/gitblit/tests/RpcTests.java           |   12 
 build.xml                                       |    8 
 src/com/gitblit/Constants.java                  |    3 
 15 files changed, 624 insertions(+), 71 deletions(-)

diff --git a/build.xml b/build.xml
index 92174d4..107fac3 100644
--- a/build.xml
+++ b/build.xml
@@ -104,6 +104,11 @@
 			</fileset>
 		</copy>
 
+		<!-- copy gitblit.properties to the WEB-INF folder.
+		     this file is only used for parsing setting descriptions. -->
+		<copy todir="${basedir}/src/WEB-INF" overwrite="true"
+			file="${basedir}/distrib/gitblit.properties" />
+
 		<!-- Compile the build tool and execute it.
 			 This downloads missing compile-time dependencies from Maven. -->
 
@@ -306,10 +311,11 @@
 		
 		<delete dir="${project.war.dir}" />		
 
-		<!-- Copy web.xml and users.properties to WEB-INF -->
+		<!-- Copy web.xml, users.properties, and gitblit.properties to WEB-INF -->
 		<copy todir="${project.war.dir}/WEB-INF">
 			<fileset dir="${basedir}/distrib">
 			 	<include name="users.properties" />
+				<include name="gitblit.properties" />
 			</fileset>
 			<fileset dir="${basedir}/src/WEB-INF">
 			 	<include name="web.xml" />
diff --git a/docs/02_rpc.mkd b/docs/02_rpc.mkd
index fbc5e44..b01eb67 100644
--- a/docs/02_rpc.mkd
+++ b/docs/02_rpc.mkd
@@ -30,10 +30,10 @@
 <tr><td>LIST_FEDERATION_RESULTS</td><td>-</td><td><em>admin</em></td><td>-</td><td>List&lt;FederationModel&gt;</td></tr>
 <tr><td>LIST_FEDERATION_PROPOSALS</td><td>-</td><td><em>admin</em></td><td>-</td><td>List&lt;FederationProposal&gt;</td></tr>
 <tr><td>LIST_FEDERATION_SETS</td><td>-</td><td><em>admin</em></td><td>-</td><td>List&lt;FederationSet&gt;</td></tr>
-<tr><td>LIST_SETTINGS</td><td>-</td><td><em>admin</em></td><td>-</td><td>Properties</td></tr>
+<tr><td>LIST_SETTINGS</td><td>-</td><td><em>admin</em></td><td>-</td><td>Map&lt;String, SettingModel&gt;</td></tr>
 </table>
 
-### RPC Response Codes
+### RPC/HTTP Response Codes
 <table>
 <tr><th>code</th><th>name</th><th>description</th></tr>
 <tr><td>200</td><td>success</td><td>Gitblit processed the request successfully</td></tr>
@@ -47,7 +47,7 @@
 ### Gitblit Manager
 
 [Gitblit Manager](http://code.google.com/p/gitblit/downloads/detail?name=%MANAGER%) is an example Java/Swing application that allows remote administration of a Gitblit server.  
-This application exercises most methods from the utility class `com.gitblit.utils.RpcUtils`.
+This application exercises many, but not all, methods from the utility class `com.gitblit.utils.RpcUtils`.
 
 ### EGit "Import from Gitblit" Feature (Planning)
 
@@ -153,4 +153,32 @@
     ]
   }
 ]
+</pre>
+
+### Example: LIST_SETTINGS
+**url**: https://localhost/rpc?req=LIST_SETTINGS  
+**response body**: Map&lt;String, SettingModel&gt;
+<pre>
+{
+  "web.siteName": {
+    "name": "web.siteName",
+    "currentValue": "",
+    "defaultValue": "",
+    "description": "Gitblit Web Settings\nIf blank Gitblit is displayed.",
+    "since": "0.5.0",
+    "caseSensitive": false,
+    "restartRequired": false,
+    "spaceDelimited": false
+  },
+  "web.summaryCommitCount": {
+    "name": "web.summaryCommitCount",
+    "currentValue": "16",
+    "defaultValue": "16",
+    "description": "The number of commits to display on the summary page\nValue must exceed 0 else default of 16 is used",
+    "since": "0.5.0",
+    "caseSensitive": false,
+    "restartRequired": false,
+    "spaceDelimited": false
+  }
+}
 </pre>
\ No newline at end of file
diff --git a/src/com/gitblit/Constants.java b/src/com/gitblit/Constants.java
index d6495e6..f3ff6c4 100644
--- a/src/com/gitblit/Constants.java
+++ b/src/com/gitblit/Constants.java
@@ -204,7 +204,8 @@
 		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_SETTINGS;
+		LIST_FEDERATION_PROPOSALS, LIST_FEDERATION_SETS, LIST_SETTINGS,
+		LIST_SERVER_STATUS;
 
 		public static RpcRequest fromName(String name) {
 			for (RpcRequest type : values()) {
diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java
index 51d5612..238c01f 100644
--- a/src/com/gitblit/GitBlit.java
+++ b/src/com/gitblit/GitBlit.java
@@ -15,9 +15,12 @@
  */
 package com.gitblit;
 
+import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileFilter;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.lang.reflect.Field;
 import java.text.MessageFormat;
 import java.util.ArrayList;
@@ -27,6 +30,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.TreeMap;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
@@ -35,6 +39,7 @@
 
 import javax.mail.Message;
 import javax.mail.MessagingException;
+import javax.servlet.ServletContext;
 import javax.servlet.ServletContextEvent;
 import javax.servlet.ServletContextListener;
 import javax.servlet.http.Cookie;
@@ -63,6 +68,8 @@
 import com.gitblit.models.Metric;
 import com.gitblit.models.ObjectCache;
 import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.ServerStatus;
+import com.gitblit.models.SettingModel;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.ByteFormat;
 import com.gitblit.utils.FederationUtils;
@@ -106,6 +113,8 @@
 
 	private RepositoryResolver<Void> repositoryResolver;
 
+	private ServletContext servletContext;
+
 	private File repositoriesFolder;
 
 	private boolean exportAll = true;
@@ -113,6 +122,10 @@
 	private IUserService userService;
 
 	private IStoredSettings settings;
+
+	private Map<String, SettingModel> settingModels;
+
+	private ServerStatus serverStatus;
 
 	private MailExecutor mailExecutor;
 
@@ -231,6 +244,13 @@
 	 */
 	public static boolean isDebugMode() {
 		return self().settings.getBoolean(Keys.web.debugMode, false);
+	}
+
+	public ServerStatus getStatus() {
+		// update heap memory status
+		serverStatus.heapAllocated = Runtime.getRuntime().totalMemory();
+		serverStatus.heapFree = Runtime.getRuntime().freeMemory();
+		return serverStatus;
 	}
 
 	/**
@@ -1253,6 +1273,86 @@
 	}
 
 	/**
+	 * Returns the descriptions/comments of the Gitblit config settings.
+	 * 
+	 * @return Map<String, SettingModel>
+	 */
+	public Map<String, SettingModel> getSettingModels() {
+		// ensure that the current values are updated in the setting models
+		for (String key : settings.getAllKeys(null)) {
+			if (settingModels.containsKey(key)) {
+				settingModels.get(key).currentValue = settings.getString(key, "");
+			}
+		}
+		return settingModels;
+	}
+
+	/**
+	 * Parse the properties file and aggregate all the comments by the setting
+	 * key. A setting model tracks the current value, the default value, the
+	 * description of the setting and and directives about the setting.
+	 * 
+	 * @return Map<String, SettingModel>
+	 */
+	private Map<String, SettingModel> loadSettingModels() {
+		Map<String, SettingModel> map = new TreeMap<String, SettingModel>();
+		try {
+			// Read bundled Gitblit properties to extract setting descriptions.
+			// This copy is pristine and only used for populating the setting
+			// models map.
+			InputStream is = servletContext.getResourceAsStream("/WEB-INF/gitblit.properties");
+			BufferedReader propertiesReader = new BufferedReader(new InputStreamReader(is));
+			StringBuilder description = new StringBuilder();
+			SettingModel setting = new SettingModel();
+			String line = null;
+			while ((line = propertiesReader.readLine()) != null) {
+				if (line.length() == 0) {
+					description.setLength(0);
+					setting = new SettingModel();
+				} else {
+					if (line.charAt(0) == '#') {
+						if (line.length() > 1) {
+							String text = line.substring(1).trim();
+							if (SettingModel.CASE_SENSITIVE.equals(text)) {
+								setting.caseSensitive = true;
+							} else if (SettingModel.RESTART_REQUIRED.equals(text)) {
+								setting.restartRequired = true;
+							} else if (SettingModel.SPACE_DELIMITED.equals(text)) {
+								setting.spaceDelimited = true;
+							} else if (text.startsWith(SettingModel.SINCE)) {
+								try {
+									setting.since = text.split(" ")[1];
+								} catch (Exception e) {
+									setting.since = text;
+								}
+							} else {
+								description.append(text);
+								description.append('\n');
+							}
+						}
+					} else {
+						String[] kvp = line.split("=", 2);
+						String key = kvp[0].trim();
+						setting.name = key;
+						setting.defaultValue = kvp[1].trim();
+						setting.currentValue = setting.defaultValue;
+						setting.description = description.toString().trim();
+						map.put(key, setting);
+						description.setLength(0);
+						setting = new SettingModel();
+					}
+				}
+			}
+			propertiesReader.close();
+		} catch (NullPointerException e) {
+			logger.error("Failed to find resource copy of gitblit.properties");
+		} catch (IOException e) {
+			logger.error("Failed to load resource copy of gitblit.properties");
+		}
+		return map;
+	}
+
+	/**
 	 * Configure the Gitblit singleton with the specified settings source. This
 	 * source may be file settings (Gitblit GO) or may be web.xml settings
 	 * (Gitblit WAR).
@@ -1265,6 +1365,7 @@
 		repositoriesFolder = new File(settings.getString(Keys.git.repositoriesFolder, "git"));
 		logger.info("Git repositories folder " + repositoriesFolder.getAbsolutePath());
 		repositoryResolver = new FileResolver<Void>(repositoriesFolder, exportAll);
+		serverStatus = new ServerStatus();
 		String realm = settings.getString(Keys.realm.userService, "users.properties");
 		IUserService loginService = null;
 		try {
@@ -1307,6 +1408,8 @@
 	 */
 	@Override
 	public void contextInitialized(ServletContextEvent contextEvent) {
+		servletContext = contextEvent.getServletContext();
+		settingModels = loadSettingModels();
 		if (settings == null) {
 			// Gitblit WAR is running in a servlet container
 			WebXmlSettings webxmlSettings = new WebXmlSettings(contextEvent.getServletContext());
diff --git a/src/com/gitblit/RpcServlet.java b/src/com/gitblit/RpcServlet.java
index 53426da..6a8c2c5 100644
--- a/src/com/gitblit/RpcServlet.java
+++ b/src/com/gitblit/RpcServlet.java
@@ -22,7 +22,6 @@
 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;
@@ -183,15 +182,10 @@
 			}
 		} 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;
+			result = GitBlit.self().getSettingModels();
+		} else if (RpcRequest.LIST_SERVER_STATUS.equals(reqType)) {
+			// return the server's status information
+			result = GitBlit.self().getStatus();
 		}
 
 		// send the result of the request
diff --git a/src/com/gitblit/client/EditUserDialog.java b/src/com/gitblit/client/EditUserDialog.java
index eacef24..0a1ddd9 100644
--- a/src/com/gitblit/client/EditUserDialog.java
+++ b/src/com/gitblit/client/EditUserDialog.java
@@ -29,6 +29,7 @@
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import javax.swing.ImageIcon;
@@ -45,9 +46,9 @@
 import javax.swing.KeyStroke;
 
 import com.gitblit.Constants.AccessRestrictionType;
-import com.gitblit.IStoredSettings;
 import com.gitblit.Keys;
 import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.SettingModel;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.StringUtils;
 
@@ -57,7 +58,7 @@
 
 	private final UserModel user;
 
-	private final IStoredSettings settings;
+	private final Map<String, SettingModel> settings;
 
 	private boolean isCreate;
 	
@@ -77,13 +78,13 @@
 
 	private Set<String> usernames;
 
-	public EditUserDialog(IStoredSettings settings) {
+	public EditUserDialog(Map<String, SettingModel> settings) {
 		this(new UserModel(""), settings);
 		this.isCreate = true;
 		setTitle(Translation.get("gb.newUser"));		
 	}
 
-	public EditUserDialog(UserModel anUser, IStoredSettings settings) {
+	public EditUserDialog(UserModel anUser, Map<String, SettingModel> settings) {
 		super();
 		this.user = new UserModel("");
 		this.settings = settings;
@@ -196,7 +197,7 @@
 			}
 		}
 
-		int minLength = settings.getInteger(Keys.realm.minPasswordLength, 5);
+		int minLength = settings.get(Keys.realm.minPasswordLength).getInteger(5);
 		if (minLength < 4) {
 			minLength = 4;
 		}
@@ -216,7 +217,7 @@
 			return false;
 		}
 		user.username = uname;
-		String type = settings.getString(Keys.realm.passwordStorage, "md5");
+		String type = settings.get(Keys.realm.passwordStorage).getString("md5");
 		if (type.equalsIgnoreCase("md5")) {
 			// store MD5 digest of password
 			user.password = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(pw));
diff --git a/src/com/gitblit/client/GitblitModel.java b/src/com/gitblit/client/GitblitModel.java
index 22d67f6..55e74b9 100644
--- a/src/com/gitblit/client/GitblitModel.java
+++ b/src/com/gitblit/client/GitblitModel.java
@@ -24,10 +24,11 @@
 
 import com.gitblit.GitBlitException.ForbiddenException;
 import com.gitblit.GitBlitException.UnauthorizedException;
-import com.gitblit.IStoredSettings;
 import com.gitblit.Keys;
 import com.gitblit.models.FederationModel;
 import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.ServerStatus;
+import com.gitblit.models.SettingModel;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.RpcUtils;
 
@@ -43,13 +44,15 @@
 
 	private volatile boolean isAdmin;
 
-	private volatile IStoredSettings settings;
+	private volatile Map<String, SettingModel> settings;
 
 	private final List<RepositoryModel> allRepositories;
 
 	private final List<UserModel> allUsers;
 
 	private final List<FederationModel> federationRegistrations;
+
+	private ServerStatus status;
 
 	public GitblitModel(String url, String account, char[] password) {
 		this.url = url;
@@ -66,8 +69,8 @@
 
 		try {
 			settings = RpcUtils.getSettings(url, account, password);
+			status = RpcUtils.getStatus(url, account, password);
 			refreshUsers();
-			refreshFederationRegistrations();
 			isAdmin = true;
 		} catch (UnauthorizedException e) {
 		} catch (ForbiddenException e) {
@@ -84,8 +87,12 @@
 		return account != null && account.equalsIgnoreCase(model.owner);
 	}
 
-	public IStoredSettings getSettings() {
+	public Map<String, SettingModel> getSettings() {
 		return settings;
+	}
+
+	public String getSettingDescription(String key) {
+		return settings.get(key).description;
 	}
 
 	public List<RepositoryModel> refreshRepositories() throws IOException {
@@ -135,7 +142,7 @@
 	}
 
 	public List<String> getFederationSets() {
-		return settings.getStrings(Keys.federation.sets);
+		return settings.get(Keys.federation.sets).getStrings();
 	}
 
 	public List<RepositoryModel> getRepositories() {
diff --git a/src/com/gitblit/client/GitblitPanel.java b/src/com/gitblit/client/GitblitPanel.java
index f0d04b7..8635b00 100644
--- a/src/com/gitblit/client/GitblitPanel.java
+++ b/src/com/gitblit/client/GitblitPanel.java
@@ -52,6 +52,7 @@
 import com.gitblit.Constants.RpcRequest;
 import com.gitblit.client.ClosableTabComponent.CloseTabListener;
 import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.SettingModel;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.StringUtils;
 
@@ -116,7 +117,6 @@
 		tabs = new JTabbedPane(JTabbedPane.BOTTOM);
 		tabs.addTab(Translation.get("gb.repositories"), createRepositoriesPanel());
 		tabs.addTab(Translation.get("gb.users"), createUsersPanel());
-		tabs.addTab(Translation.get("gb.federation"), new JPanel());
 		tabs.addTab(Translation.get("gb.settings"), createSettingsPanel());
 
 		setLayout(new BorderLayout());
@@ -380,9 +380,11 @@
 	}
 
 	private JPanel createSettingsPanel() {
+		final SettingPanel settingPanel = new SettingPanel();
 		settingsModel = new SettingsModel();
 		defaultSettingsSorter = new TableRowSorter<SettingsModel>(settingsModel);
 		settingsTable = Utils.newTable(settingsModel);
+		settingsTable.setDefaultRenderer(SettingModel.class, new SettingCellRenderer());
 		String name = settingsTable.getColumnName(UsersModel.Columns.Name.ordinal());
 		settingsTable.setRowHeight(nameRenderer.getFont().getSize() + 8);
 		settingsTable.getColumn(name).setCellRenderer(nameRenderer);
@@ -398,6 +400,14 @@
 				boolean selected = settingsTable.getSelectedRow() > -1;
 				boolean singleSelection = settingsTable.getSelectedRows().length == 1;
 				// TODO enable/disable setting buttons
+				if (singleSelection) {
+					int viewRow = settingsTable.getSelectedRow();
+					int modelRow = settingsTable.convertRowIndexToModel(viewRow);
+					SettingModel setting = settingsModel.get(modelRow);
+					settingPanel.setSetting(setting);
+				} else {
+					settingPanel.clear();
+				}
 			}
 		});
 
@@ -420,6 +430,7 @@
 		JPanel settingsTablePanel = new JPanel(new BorderLayout(margin, margin));
 		settingsTablePanel.add(settingFilterPanel, BorderLayout.NORTH);
 		settingsTablePanel.add(new JScrollPane(settingsTable), BorderLayout.CENTER);
+		settingsTablePanel.add(settingPanel, BorderLayout.SOUTH);
 
 		JPanel settingsControls = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0));
 		// TODO update setting?
diff --git a/src/com/gitblit/client/SettingCellRenderer.java b/src/com/gitblit/client/SettingCellRenderer.java
new file mode 100644
index 0000000..d164fb1
--- /dev/null
+++ b/src/com/gitblit/client/SettingCellRenderer.java
@@ -0,0 +1,67 @@
+/*
+ * 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.Color;
+import java.awt.Component;
+import java.awt.Font;
+
+import javax.swing.JTable;
+import javax.swing.table.DefaultTableCellRenderer;
+
+import com.gitblit.models.SettingModel;
+
+/**
+ * SettingModel cell renderer that indicates if a setting is the default or
+ * modified.
+ * 
+ * @author James Moger
+ * 
+ */
+public class SettingCellRenderer extends DefaultTableCellRenderer {
+
+	private static final long serialVersionUID = 1L;
+
+	private final Font defaultFont;
+
+	private final Font modifiedFont;
+
+	public SettingCellRenderer() {
+		defaultFont = getFont();
+		modifiedFont = defaultFont.deriveFont(Font.BOLD);
+	}
+
+	public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
+			boolean hasFocus, int row, int column) {
+		super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
+		if (value instanceof SettingModel) {
+			SettingModel setting = (SettingModel) value;
+			if (setting.isDefaultValue()) {
+				this.setFont(defaultFont);
+				if (!isSelected) {
+					this.setForeground(Color.BLACK);
+				}
+			} else {
+				this.setFont(modifiedFont);
+				if (!isSelected) {
+					this.setForeground(Color.BLUE);
+				}
+			}
+			this.setText(setting.getString(""));
+		}
+		return this;
+	}
+}
\ No newline at end of file
diff --git a/src/com/gitblit/client/SettingPanel.java b/src/com/gitblit/client/SettingPanel.java
new file mode 100644
index 0000000..8d07dc8
--- /dev/null
+++ b/src/com/gitblit/client/SettingPanel.java
@@ -0,0 +1,114 @@
+/*
+ * 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.Font;
+import java.awt.GridLayout;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+import javax.swing.SwingConstants;
+
+import com.gitblit.models.SettingModel;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * This panel displays the metadata for a particular setting.
+ * 
+ * @author James Moger
+ */
+public class SettingPanel extends JPanel {
+
+	private static final long serialVersionUID = 1L;
+	private JTextArea descriptionArea;
+	private JLabel settingName;
+	private JLabel settingDefault;
+	private JLabel sinceVersion;
+	private JLabel directives;
+
+	public SettingPanel() {
+		super();
+		initialize();
+	}
+
+	private void initialize() {
+		descriptionArea = new JTextArea();
+		descriptionArea.setRows(6);
+		descriptionArea.setFont(new Font("monospaced", Font.PLAIN, 11));
+
+		settingName = new JLabel(" ");
+		settingName.setFont(settingName.getFont().deriveFont(Font.BOLD));
+
+		settingDefault = new JLabel(" ");
+
+		sinceVersion = new JLabel(" ", SwingConstants.RIGHT);
+		sinceVersion.setForeground(new Color(0, 0x80, 0));
+
+		directives = new JLabel(" ", SwingConstants.RIGHT);
+		directives.setFont(directives.getFont().deriveFont(Font.ITALIC));
+
+		JPanel settingParameters = new JPanel(new GridLayout(2, 2, 0, 0));
+		settingParameters.add(settingName);
+		settingParameters.add(sinceVersion);
+		settingParameters.add(settingDefault, BorderLayout.CENTER);
+		settingParameters.add(directives);
+
+		JPanel settingPanel = new JPanel(new BorderLayout(5, 5));
+		settingPanel.add(settingParameters, BorderLayout.NORTH);
+		settingPanel.add(new JScrollPane(descriptionArea), BorderLayout.CENTER);
+		setLayout(new BorderLayout(0, 0));
+		add(settingPanel, BorderLayout.CENTER);
+	}
+
+	public void setSetting(SettingModel setting) {
+		settingName.setText(setting.name);
+		if (setting.since == null) {
+			sinceVersion.setText("custom");
+		} else {			
+			sinceVersion.setText("since " + setting.since);
+		}
+		settingDefault.setText("default: " + setting.defaultValue);
+
+		List<String> values = new ArrayList<String>();
+		if (setting.caseSensitive) {
+			values.add("CASE-SENSITIVE");
+		}
+		if (setting.spaceDelimited) {
+			values.add("SPACE-DELIMITED");
+		}
+		if (setting.restartRequired) {
+			values.add("RESTART REQUIRED");
+		}
+		directives.setText(StringUtils.flattenStrings(values, ", "));
+
+		descriptionArea.setText(setting.description);
+		descriptionArea.setCaretPosition(0);
+	}
+
+	public void clear() {
+		settingName.setText(" ");
+		settingDefault.setText(" ");
+		sinceVersion.setText(" ");
+		directives.setText(" ");
+		descriptionArea.setText("");
+	}
+}
diff --git a/src/com/gitblit/client/SettingsModel.java b/src/com/gitblit/client/SettingsModel.java
index 2c7bff8..af3c1b6 100644
--- a/src/com/gitblit/client/SettingsModel.java
+++ b/src/com/gitblit/client/SettingsModel.java
@@ -18,13 +18,14 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 import javax.swing.table.AbstractTableModel;
 
-import com.gitblit.IStoredSettings;
+import com.gitblit.models.SettingModel;
 
 /**
- * Table model of IStoredSettings.
+ * Table model of Map<String, SettingModel>.
  * 
  * @author James Moger
  * 
@@ -33,7 +34,7 @@
 
 	private static final long serialVersionUID = 1L;
 
-	IStoredSettings settings;
+	Map<String, SettingModel> settings;
 
 	List<String> keys;
 
@@ -50,16 +51,16 @@
 		this(null);
 	}
 
-	public SettingsModel(IStoredSettings settings) {
+	public SettingsModel(Map<String, SettingModel> settings) {
 		setSettings(settings);
 	}
 
-	public void setSettings(IStoredSettings settings) {
+	public void setSettings(Map<String, SettingModel> settings) {
 		this.settings = settings;
 		if (settings == null) {
 			keys = new ArrayList<String>();
 		} else {
-			keys = new ArrayList<String>(settings.getAllKeys(null));
+			keys = new ArrayList<String>(settings.keySet());
 			Collections.sort(keys);
 		}
 	}
@@ -92,19 +93,28 @@
 	 * @return the Object.class
 	 */
 	public Class<?> getColumnClass(int columnIndex) {
+		if (Columns.Value.ordinal() == columnIndex) {
+			return SettingModel.class;
+		}
 		return String.class;
 	}
 
 	@Override
 	public Object getValueAt(int rowIndex, int columnIndex) {
 		String key = keys.get(rowIndex);
+		SettingModel setting = settings.get(key);
 		Columns col = Columns.values()[columnIndex];
 		switch (col) {
 		case Name:
 			return key;
 		case Value:
-			return settings.getString(key, "");
+			return setting;
 		}
 		return null;
 	}
+
+	public SettingModel get(int modelRow) {
+		String key = keys.get(modelRow);
+		return settings.get(key);
+	}
 }
diff --git a/src/com/gitblit/models/ServerStatus.java b/src/com/gitblit/models/ServerStatus.java
new file mode 100644
index 0000000..b1dc52d
--- /dev/null
+++ b/src/com/gitblit/models/ServerStatus.java
@@ -0,0 +1,69 @@
+/*
+ * 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.models;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * ServerStatus encapsulates runtime status information about the server
+ * including the system environment.
+ * 
+ * @author James Moger
+ * 
+ */
+public class ServerStatus implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	public final Date bootDate;
+	
+	public final long heapSize;
+
+	public final Map<String, String> systemProperties;
+	
+	public volatile long heapAllocated;
+	
+	public volatile long heapFree;
+
+	public ServerStatus() {
+		bootDate = new Date();
+		
+		heapSize = Runtime.getRuntime().maxMemory();
+		
+		systemProperties = new HashMap<String, String>();
+		put("file.encoding");
+		put("java.home");
+		put("java.io.tmpdir");
+		put("java.runtime.name");
+		put("java.runtime.version");
+		put("java.vendor");
+		put("java.version");
+		put("java.vm.info");
+		put("java.vm.name");
+		put("java.vm.vendor");
+		put("java.vm.version");
+		put("os.arch");
+		put("os.name");
+		put("os.version");
+	}
+
+	private void put(String key) {
+		systemProperties.put(key, System.getProperty(key));
+	}
+}
diff --git a/src/com/gitblit/models/SettingModel.java b/src/com/gitblit/models/SettingModel.java
new file mode 100644
index 0000000..8a5c0c6
--- /dev/null
+++ b/src/com/gitblit/models/SettingModel.java
@@ -0,0 +1,144 @@
+/*
+ * 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.models;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.gitblit.utils.StringUtils;
+
+/**
+ * SettingModel represents a setting and all its metadata: name, current value,
+ * default value, description, and directives.
+ * 
+ * @author James Moger
+ */
+public class SettingModel implements Serializable {
+
+	public static final String SPACE_DELIMITED = "SPACE-DELIMITED";
+
+	public static final String CASE_SENSITIVE = "CASE-SENSITIVE";
+
+	public static final String RESTART_REQUIRED = "RESTART REQUIRED";
+
+	public static final String SINCE = "SINCE";
+
+	public String name;
+	public volatile String currentValue;
+	public String defaultValue;
+	public String description;
+	public String since;
+	public boolean caseSensitive;
+	public boolean restartRequired;
+	public boolean spaceDelimited;
+
+	private static final long serialVersionUID = 1L;
+
+	public SettingModel() {
+	}
+
+	/**
+	 * Returns true if the current value is the default value.
+	 * 
+	 * @return true if current value is the default value
+	 */
+	public boolean isDefaultValue() {
+		return (currentValue != null && currentValue.equals(defaultValue))
+				|| currentValue.trim().length() == 0;
+	}
+
+	/**
+	 * Returns the boolean value for the currentValue. If the currentValue can
+	 * not be interpreted as a boolean, the defaultValue is returned.
+	 * 
+	 * @param defaultValue
+	 * @return key value or defaultValue
+	 */
+	public boolean getBoolean(boolean defaultValue) {
+		if (!StringUtils.isEmpty(currentValue)) {
+			return Boolean.parseBoolean(currentValue.trim());
+		}
+		return defaultValue;
+	}
+
+	/**
+	 * Returns the integer value for the currentValue. If the currentValue can
+	 * not be interpreted as an integer, the defaultValue is returned.
+	 * 
+	 * @param defaultValue
+	 * @return key value or defaultValue
+	 */
+	public int getInteger(int defaultValue) {
+		try {
+			if (!StringUtils.isEmpty(currentValue)) {
+				return Integer.parseInt(currentValue.trim());
+			}
+		} catch (NumberFormatException e) {
+		}
+		return defaultValue;
+	}
+
+	/**
+	 * Returns the char value for currentValue. If the currentValue can not be
+	 * interpreted as a char, the defaultValue is returned.
+	 * 
+	 * @param defaultValue
+	 * @return key value or defaultValue
+	 */
+	public char getChar(char defaultValue) {
+		if (!StringUtils.isEmpty(currentValue)) {
+			return currentValue.trim().charAt(0);
+		}
+		return defaultValue;
+	}
+
+	/**
+	 * Returns the string value for currentValue. If the currentValue is null,
+	 * the defaultValue is returned.
+	 * 
+	 * @param defaultValue
+	 * @return key value or defaultValue
+	 */
+	public String getString(String defaultValue) {
+		if (currentValue != null) {
+			return currentValue.trim();
+		}
+		return defaultValue;
+	}
+
+	/**
+	 * Returns a list of space-separated strings from the specified key.
+	 * 
+	 * @return list of strings
+	 */
+	public List<String> getStrings() {
+		return getStrings(" ");
+	}
+
+	/**
+	 * Returns a list of strings from the currentValue using the specified
+	 * string separator.
+	 * 
+	 * @param separator
+	 * @return list of strings
+	 */
+	public List<String> getStrings(String separator) {
+		List<String> strings = new ArrayList<String>();
+		strings = StringUtils.getStringsFromValue(currentValue, separator);
+		return strings;
+	}
+}
diff --git a/src/com/gitblit/utils/RpcUtils.java b/src/com/gitblit/utils/RpcUtils.java
index eb28c0f..440dabd 100644
--- a/src/com/gitblit/utils/RpcUtils.java
+++ b/src/com/gitblit/utils/RpcUtils.java
@@ -21,15 +21,15 @@
 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;
 import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.ServerStatus;
+import com.gitblit.models.SettingModel;
 import com.gitblit.models.UserModel;
 import com.google.gson.reflect.TypeToken;
 
@@ -57,6 +57,9 @@
 	}.getType();
 
 	private static final Type SETS_TYPE = new TypeToken<Collection<FederationSet>>() {
+	}.getType();
+
+	private static final Type SETTINGS_TYPE = new TypeToken<Map<String, SettingModel>>() {
 	}.getType();
 
 	/**
@@ -338,15 +341,31 @@
 	 * @param serverUrl
 	 * @param account
 	 * @param password
-	 * @return an IStoredSettings object
+	 * @return an Map<String, SettingModel> object
 	 * @throws IOException
 	 */
-	public static IStoredSettings getSettings(String serverUrl, String account, char[] password)
-			throws IOException {
+	public static Map<String, SettingModel> 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);
+		Map<String, SettingModel> settings = JsonUtils.retrieveJson(url, SETTINGS_TYPE, account,
+				password);
 		return settings;
+	}
+
+	/**
+	 * Retrieves the server status object.
+	 * 
+	 * @param serverUrl
+	 * @param account
+	 * @param password
+	 * @return an ServerStatus object
+	 * @throws IOException
+	 */
+	public static ServerStatus getStatus(String serverUrl, String account, char[] password)
+			throws IOException {
+		String url = asLink(serverUrl, RpcRequest.LIST_SERVER_STATUS);
+		ServerStatus status = JsonUtils.retrieveJson(url, ServerStatus.class, account, password);
+		return status;
 	}
 
 	/**
@@ -368,32 +387,5 @@
 		String json = JsonUtils.toJsonString(object);
 		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/tests/com/gitblit/tests/RpcTests.java b/tests/com/gitblit/tests/RpcTests.java
index 450b597..3c2f61f 100644
--- a/tests/com/gitblit/tests/RpcTests.java
+++ b/tests/com/gitblit/tests/RpcTests.java
@@ -23,11 +23,12 @@
 
 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;
 import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.ServerStatus;
+import com.gitblit.models.SettingModel;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.RpcUtils;
 
@@ -206,7 +207,12 @@
 	}
 
 	public void testSettings() throws Exception {
-		IStoredSettings settings = RpcUtils.getSettings(url, account, password.toCharArray());
-		assertTrue("No settings were retrieved!", settings.getAllKeys(null).size() > 0);
+		Map<String, SettingModel> settings = RpcUtils.getSettings(url, account, password.toCharArray());
+		assertTrue("No settings were retrieved!", settings != null);
+	}
+	
+	public void testServerStatus() throws Exception {
+		ServerStatus status = RpcUtils.getStatus(url, account, password.toCharArray());
+		assertTrue("No status was retrieved!", status != null);
 	}
 }

--
Gitblit v1.9.1