James Moger
2011-10-12 da0269b4bd57bf90877446d9f991247bc1ad2f64
RPC Client: Create/Edit Repository & User. Partially working.

Added new request type to retrieve Gitblit settings to implement the
password preferences (minLength, storage type) and federation sets.
3 files added
11 files modified
1162 ■■■■■ changed files
docs/02_rpc.mkd 1 ●●●● patch | view | raw | blame | history
src/com/gitblit/Constants.java 2 ●●● patch | view | raw | blame | history
src/com/gitblit/RpcServlet.java 12 ●●●●● patch | view | raw | blame | history
src/com/gitblit/client/EditRepositoryDialog.java 224 ●●●●● patch | view | raw | blame | history
src/com/gitblit/client/EditUserDialog.java 237 ●●●●● patch | view | raw | blame | history
src/com/gitblit/client/GitblitClient.java 8 ●●●●● patch | view | raw | blame | history
src/com/gitblit/client/GitblitClientLauncher.java 8 ●●●●● patch | view | raw | blame | history
src/com/gitblit/client/GitblitPanel.java 399 ●●●●● patch | view | raw | blame | history
src/com/gitblit/client/JPalette.java 177 ●●●●● patch | view | raw | blame | history
src/com/gitblit/client/NameRenderer.java 4 ●●●● patch | view | raw | blame | history
src/com/gitblit/utils/JsonUtils.java 19 ●●●●● patch | view | raw | blame | history
src/com/gitblit/utils/RpcUtils.java 61 ●●●● patch | view | raw | blame | history
src/com/gitblit/wicket/GitBlitWebApp.properties 4 ●●●● patch | view | raw | blame | history
tests/com/gitblit/tests/RpcTests.java 6 ●●●●● patch | view | raw | blame | history
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
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()) {
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
src/com/gitblit/client/EditRepositoryDialog.java
New file
@@ -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;
    }
}
src/com/gitblit/client/EditUserDialog.java
New file
@@ -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;
    }
}
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() {
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();
                    }
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
    }
}
src/com/gitblit/client/JPalette.java
New file
@@ -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();
        }
    }
}
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);
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.
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";
        }
    }
}
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
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);
    }
}