org.sakaiproject.nakamura.grouper.changelog.HttpNakamuraManagerImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.sakaiproject.nakamura.grouper.changelog.HttpNakamuraManagerImpl.java

Source

/* Licensed to the Sakai Foundation (SF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The SF licenses this file
 * to you 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 org.sakaiproject.nakamura.grouper.changelog;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;

import net.sf.json.JSONArray;
import net.sf.json.JSONObject;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.nakamura.grouper.changelog.api.GroupIdManager;
import org.sakaiproject.nakamura.grouper.changelog.api.NakamuraManager;
import org.sakaiproject.nakamura.grouper.changelog.api.WorldConstants;
import org.sakaiproject.nakamura.grouper.changelog.exceptions.GroupModificationException;
import org.sakaiproject.nakamura.grouper.changelog.exceptions.UserModificationException;
import org.sakaiproject.nakamura.grouper.changelog.log.AuditLogUtils;
import org.sakaiproject.nakamura.grouper.changelog.util.NakamuraHttpUtils;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.MapMaker;

import edu.internet2.middleware.grouper.SubjectFinder;
import edu.internet2.middleware.grouper.exception.GrouperException;
import edu.internet2.middleware.subject.Subject;

/**
 * Shared functionality for the GroupAdapter classes goes in here.
 */
public class HttpNakamuraManagerImpl implements NakamuraManager {

    private static Log log = LogFactory.getLog(HttpNakamuraManagerImpl.class);

    // URI for the OAE user and group management servlets.
    public static final String USER_MANAGER_URI = "/system/userManager";
    public static final String GROUP_CREATE_URI = USER_MANAGER_URI + "/group.create.json";
    public static final String GROUP_PATH_PREFIX = USER_MANAGER_URI + "/group";
    public static final String WORLD_CREATE_URI = "/system/world/create";
    public static final String USER_CREATE_URI = USER_MANAGER_URI + "/user.create.json";

    // Nakamura Batch servlet takes a JSONArray of JSONObjects that each represent a request
    public static final String BATCH_URI = "/system/batch";
    public static final String BATCH_REQUESTS_PARAM = "requests";
    public static final String PARAMETERS_PARAM = "parameters";

    // Creating worlds
    public static final String DATA_PARAM = "data";
    public static final String URL_PARAM = "url";
    public static final String ID_PARAM = "id";
    public static final String TITLE_PARAM = "title";
    public static final String TAGS_PARAM = "tags";
    public static final String DESCRIPTION_PARAM = "description";
    public static final String VISIBILITY_PARAM = "visibility";
    public static final String JOINABILITY_PARAM = "joinability";
    public static final String WORLD_TEMPLATE_PARAM = "worldTemplate";
    public static final String MESSAGE_PARAM = "message";
    public static final String USERS_TO_ADD_PARAM = "usersToAdd";

    public static final String COURSE_DEFAULT_ADMIN_ROLE = "lecturer";
    public static final String SIMPLE_GROUP_DEFAULT_ADMIN_ROLE = "manager";

    // Creating Users
    public static final String NAME_PARAM = ":name";
    public static final String PWD_PARAM = "pwd";
    public static final String PWD_CONFIRM_PARAM = "pwdConfirm";
    public static final String FIRST_NAME_PARAM = "firstName";
    public static final String LAST_NAME_PARAM = "lastName";
    public static final String EMAIL_PARAM = "email";
    public static final String TIMEZONE_PARAM = "timezone";
    public static final String PROFILE_IMPORT_PARAM = ":sakai:profile-import";
    public static final String LOCALE_PARAM = "locale";

    // Sling params
    public static final String OPERATION_PARAM = ":operation";
    public static final String METHOD_PARAM = "method";
    public static final String VIEWER_PARAM = ":viewer";
    public static final String MEMBER_PARAM = ":member";
    public static final String MANAGER_PARAM = ":manager";
    public static final String VIEWER_DELETE_PARAM = ":viewer@Delete";
    public static final String MEMBER_DELETE_PARAM = ":member@Delete";

    // Passed along with every request
    public static final String CHARSET_PARAM = "_charset_";
    public static final String UTF_8 = "utf-8";

    // Creates new files
    public static final String CREATE_FILE_URI = "/system/pool/createfile";

    // Properties stored on authorizables in Sakai OAE
    public static final String GROUPER_NAME_PROP = "grouper:name";
    public static final String GROUPER_PROVISIONED_PROP = "grouper:provisioned";

    // Connection info for the OAE server
    public URL url;
    public String username;
    public String password;

    public boolean createUsers = false;

    // User cache
    protected Map<String, Boolean> userExistsInSakai;

    // Group cache
    protected Map<String, Boolean> groupExistsInSakai;

    // Configurable subject attributes. Used when creating users in OAE.
    public String firstNameAttribute;
    public String lastNameAttribute;
    public String emailAttribute;
    public String defaultEmailDomain;

    // Sakai OAE pseudoGroup suffixes (lecturer, ta, student...)
    public Set<String> pseudoGroupSuffixes;

    // Mock out calls to Sakai
    public boolean dryrun = false;

    // Convert grouper names to Sakai OAE groupIds
    public GroupIdManager groupIdAdapter;

    public HttpNakamuraManagerImpl() {
        userExistsInSakai = new MapMaker().expireAfterWrite(30, TimeUnit.SECONDS).makeMap();
        groupExistsInSakai = new MapMaker().expireAfterWrite(30, TimeUnit.SECONDS).makeMap();
    }

    /**
     * Create the full set of objects that are necessary to have a working
     * course in Sakai OAE.
     *
     * Written on the back of the amazing John King who pulled out all of this
     * for nakamura/testscripts/SlingRuby/dataload/full_group_creator.rb
     * @throws GroupModificationException
     */
    @Override
    public void createWorld(String grouperName, String worldId, String title, String description, String[] tags,
            String visibility, String joinability, String template, String message,
            Map<String, String> usersRolesToAdd) throws GroupModificationException {

        JSONObject params = makeCreateWorldParams(worldId, worldId, tags, worldId, visibility, joinability,
                template, message, usersRolesToAdd);

        PostMethod method = new PostMethod(url + WORLD_CREATE_URI);
        method.setParameter(DATA_PARAM, params.toString());
        method.setParameter(CHARSET_PARAM, UTF_8);

        try {
            if (!dryrun) {
                HttpClient client = NakamuraHttpUtils.getHttpClient(url, username, password);
                NakamuraHttpUtils.http(client, method);
                AuditLogUtils.audit(AuditLogUtils.GROUP_CREATED, null, worldId, description, AuditLogUtils.SUCCESS);
                setGrouperNameProperties(worldId, grouperName);
            }
        } catch (GroupModificationException gme) {
            AuditLogUtils.audit(AuditLogUtils.GROUP_CREATED, null, worldId, description, AuditLogUtils.FAILURE);
            throw gme;
        }

        log.info("Successfully created the OAE world " + worldId + " for " + grouperName);
    }

    /**
     * Set the grouper:* properties on the group authorizables in OAE
     * @param worldId
     * @param grouperName
     * @throws GroupModificationException
     */
    private void setGrouperNameProperties(String worldId, String grouperName) throws GroupModificationException {
        List<String> roles = getRoles(worldId);
        for (String role : roles) {
            setProperties(worldId + "-" + role, ImmutableMap.of(GROUPER_NAME_PROP,
                    StringUtils.substringBeforeLast(grouperName, ":") + ":" + role, GROUPER_PROVISIONED_PROP, "1"));
        }
    }

    /**
     * For a description of the params see
     * {@link NakamuraManager#createWorld(String, String, String, String, String[], String, String, String, String, Map)}
     * @return the JSONObject the represents the new World request.
     */
    private JSONObject makeCreateWorldParams(String id, String title, String[] tags, String description,
            String visibility, String joinability, String template, String message,
            Map<String, String> usersRolesToAdd) {
        JSONObject params = new JSONObject();
        params.put(ID_PARAM, id);
        params.put(TITLE_PARAM, title);
        params.put(TAGS_PARAM, "[\"" + StringUtils.join(tags, ",") + "\"]");
        params.put(DESCRIPTION_PARAM, description);
        params.put(VISIBILITY_PARAM, visibility);
        params.put(JOINABILITY_PARAM, joinability);
        params.put(WORLD_TEMPLATE_PARAM, template);
        params.put(MESSAGE_PARAM, message);
        JSONArray toAdd = new JSONArray();
        for (Entry<String, String> entry : usersRolesToAdd.entrySet()) {
            JSONObject jsonEntry = new JSONObject();
            jsonEntry.put("userid", entry.getKey());
            jsonEntry.put("role", entry.getValue());
            toAdd.add(jsonEntry);
        }
        params.put(USERS_TO_ADD_PARAM, toAdd.toString());
        return params;
    }

    /*
     * POST http://localhost:8080/system/userManager/group/groupId.update.json :member=subjectId
     */
    public void addMembership(String nakamuraGroupId, String memberId) throws GroupModificationException {
        String parentGroupId = groupIdAdapter.getWorldId(nakamuraGroupId);
        String role = StringUtils.substringAfterLast(nakamuraGroupId, "-");
        PostMethod method = new PostMethod(url.toString() + getUpdateURI(nakamuraGroupId));
        method.addParameter(MEMBER_PARAM, memberId);
        method.addParameter(VIEWER_PARAM, memberId);
        method.addParameter(CHARSET_PARAM, UTF_8);
        try {
            if (!dryrun) {
                NakamuraHttpUtils.http(NakamuraHttpUtils.getHttpClient(url, username, password), method);
                AuditLogUtils.audit(AuditLogUtils.USER_ADDED, memberId, parentGroupId, role, AuditLogUtils.SUCCESS);
            }
            log.info("Added subjectId=" + memberId + " to group=" + nakamuraGroupId);
        } catch (GrouperException ge) {
            AuditLogUtils.audit(AuditLogUtils.USER_ADDED, memberId, parentGroupId, role, AuditLogUtils.FAILURE);
            throw ge;
        }
    }

    public void addMemberships(String nakamuraGroupId, List<String> memberIds) throws GroupModificationException {

        if (memberIds.isEmpty()) {
            return;
        } else if (memberIds.size() == 1) {
            addMembership(nakamuraGroupId, memberIds.get(0));
            return;
        }

        String parentGroupId = groupIdAdapter.getWorldId(nakamuraGroupId);
        String role = StringUtils.substringAfterLast(nakamuraGroupId, "-");

        JSONArray requests = new JSONArray();
        for (String memberId : memberIds) {
            JSONObject req = new JSONObject();
            req.put(METHOD_PARAM, "POST");
            req.put(CHARSET_PARAM, UTF_8);
            req.put(URL_PARAM, getUpdateURI(nakamuraGroupId));

            JSONObject params = new JSONObject();
            params.put(MEMBER_PARAM, memberId);
            params.put(VIEWER_PARAM, memberId);
            params.put(CHARSET_PARAM, UTF_8);

            req.put(PARAMETERS_PARAM, params);
            requests.add(req);
        }
        try {
            // Use the OAE batch service to add multiple memberships in one HTTP POST
            batchPost(requests);
            for (String memberId : memberIds) {
                AuditLogUtils.audit(AuditLogUtils.USER_ADDED, memberId, parentGroupId, role, AuditLogUtils.SUCCESS);
            }
            log.info("Added subjectId=" + StringUtils.join(memberIds.toArray(), ",") + " to group="
                    + nakamuraGroupId);
        } catch (GrouperException ge) {
            for (String memberId : memberIds) {
                AuditLogUtils.audit(AuditLogUtils.USER_ADDED, memberId, parentGroupId, role, AuditLogUtils.FAILURE);
            }
            throw ge;
        }
    }

    /*
     * POST http://localhost:8080/system/userManager/group/groupId.update.json?
     * :member@Delete=memberId
     * :viewer@Delete=memberId
     */
    public void deleteMembership(String nakamuraGroupId, String memberId) throws GroupModificationException {
        String parentGroupId = groupIdAdapter.getWorldId(nakamuraGroupId);
        String role = StringUtils.substringAfterLast(nakamuraGroupId, "-");
        PostMethod method = new PostMethod(url.toString() + getUpdateURI(nakamuraGroupId));
        method.addParameter(MEMBER_DELETE_PARAM, memberId);
        method.addParameter(VIEWER_DELETE_PARAM, memberId);
        method.addParameter(CHARSET_PARAM, UTF_8);
        try {
            if (!dryrun) {
                NakamuraHttpUtils.http(NakamuraHttpUtils.getHttpClient(url, username, password), method);
                AuditLogUtils.audit(AuditLogUtils.USER_DELETED, memberId, parentGroupId, role,
                        AuditLogUtils.SUCCESS);
            }
            log.info("Deleted subjectId=" + memberId + " from group=" + nakamuraGroupId);
        } catch (GrouperException ge) {
            AuditLogUtils.audit(AuditLogUtils.USER_DELETED, memberId, parentGroupId, role, AuditLogUtils.FAILURE);
            throw ge;
        }
    }

    public boolean groupExists(String groupId) {
        if (dryrun) {
            return false;
        }

        if (!groupExistsInSakai.containsKey(groupId)) {
            HttpClient client = NakamuraHttpUtils.getHttpClient(url, username, password);
            String groupUrl = url.toString() + GROUP_PATH_PREFIX + "/" + groupId + ".json";
            GetMethod method = new GetMethod(groupUrl);
            try {
                int responseCode = client.executeMethod(method);
                if (responseCode == HttpStatus.SC_OK) {
                    groupExistsInSakai.put(groupId, Boolean.TRUE);
                }
                log.debug(responseCode + " : " + groupUrl);
            } catch (Exception e) {
                log.error(e.getMessage());
                throw new GrouperException(e.getMessage());
            }
        }
        return groupExistsInSakai.containsKey(groupId);
    }

    /*
     * POST http://localhost:8080/system/userManager/group/groupId.update.json?key1=value1
     */
    public void setProperty(String groupId, String key, String value) throws GroupModificationException {
        setProperties(groupId, ImmutableMap.of(key, value));
    }

    /*
     * POST http://localhost:8080/system/userManager/group/groupId.update.json?key1=value1&key2=value2...
     */
    public void setProperties(String groupId, Map<String, String> properties) throws GroupModificationException {
        PostMethod method = new PostMethod(url.toString() + getUpdateURI(groupId));
        for (Entry<String, String> entry : properties.entrySet()) {
            method.setParameter(entry.getKey(), entry.getValue());
        }
        method.setParameter(CHARSET_PARAM, UTF_8);
        try {
            if (!dryrun) {
                NakamuraHttpUtils.http(NakamuraHttpUtils.getHttpClient(url, username, password), method);
                for (Entry<String, String> entry : properties.entrySet()) {
                    AuditLogUtils.audit(AuditLogUtils.GROUP_MODIFIED, null, groupId,
                            entry.getKey() + "=" + entry.getValue(), AuditLogUtils.SUCCESS);
                    log.info("Set " + groupId + " : " + entry.getKey() + "=" + entry.getValue());
                }
            }
        } catch (GrouperException ge) {
            AuditLogUtils.audit(AuditLogUtils.GROUP_MODIFIED, null, groupId, properties.toString(),
                    AuditLogUtils.FAILURE);
            throw ge;
        }
    }

    /**
     * Create a user in OAE if it doesn't exist.
     * @param userId
     * @throws Exception
     */
    public void createUser(String userId) throws UserModificationException {

        if (dryrun || userExists(userId)) {
            return;
        }
        HttpClient client = NakamuraHttpUtils.getHttpClient(url, username, password);

        String fullName = null;
        try {
            // throws exception if not found or not unique
            Subject subject = SubjectFinder.findByIdOrIdentifier(userId, true);

            String randomPassword = UUID.randomUUID().toString();
            PostMethod method = new PostMethod(url.toString() + USER_CREATE_URI);

            // Get properties from the Grouper Subject
            String firstName = subject.getAttributeValue(firstNameAttribute);
            if (firstName == null) {
                firstName = "Firstname";
            }
            String lastName = subject.getAttributeValue(lastNameAttribute);
            if (lastName == null) {
                lastName = "Lastname";
            }

            fullName = firstName + " " + lastName;

            String email = subject.getAttributeValue(emailAttribute);
            if (email == null) {
                email = userId + "@" + defaultEmailDomain;
            }
            // Fill in the template
            String profileTemplate = "{\"basic\":{\"elements\":{\"firstName\":{\"value\":\"FIRSTNAME\"},\"lastName\":{\"value\":\"LASTNAME\"},\"email\":{\"value\":\"EMAIL\"}},\"access\":\"everybody\"},\"email\":\"EMAIL\"}}";
            profileTemplate = profileTemplate.replaceAll("FIRSTNAME", firstName).replaceAll("LASTNAME", lastName)
                    .replaceAll("EMAIL", email);

            method.addParameter(NAME_PARAM, userId);
            method.addParameter(PWD_PARAM, randomPassword);
            method.addParameter(PWD_CONFIRM_PARAM, randomPassword);
            method.addParameter(FIRST_NAME_PARAM, firstName);
            method.addParameter(LAST_NAME_PARAM, lastName);
            method.addParameter(EMAIL_PARAM, email);
            method.addParameter(TIMEZONE_PARAM, "America/New_York");
            method.addParameter(LOCALE_PARAM, "en_US");
            method.addParameter(PROFILE_IMPORT_PARAM, profileTemplate);

            NakamuraHttpUtils.http(client, method);
            userExistsInSakai.put(userId, Boolean.TRUE);
            log.info("Created a user in Sakai OAE for " + userId);
            AuditLogUtils.audit(AuditLogUtils.USER_CREATED, userId, null, fullName, AuditLogUtils.SUCCESS);
        } catch (Exception e) {
            log.error("Unable to create the user in Sakai OAE", e);
            AuditLogUtils.audit(AuditLogUtils.USER_CREATED, userId, null, fullName, AuditLogUtils.FAILURE);
            throw new UserModificationException(e.getMessage());
        }
    }

    /**
     * Send a batch request to delete the group and its pseudoGroups
     * @param groupId id of the group or one of its pseudoGroups
     * @param groupName
     * @throws GroupModificationException
     */
    public void deleteGroup(String groupId, String groupName) throws GroupModificationException {
        String parentGroupId = groupIdAdapter.getWorldId(groupId);
        HttpClient client = NakamuraHttpUtils.getHttpClient(url, username, password);

        JSONArray batchRequests = new JSONArray();
        // Add the delete requests for the parent group
        JSONObject request = new JSONObject();
        request.put(METHOD_PARAM, "POST");
        request.put(CHARSET_PARAM, UTF_8);
        request.put(URL_PARAM, getDeleteURI(parentGroupId));
        JSONObject params = new JSONObject();
        params.put(OPERATION_PARAM, "delete");
        request.put(PARAMETERS_PARAM, params);
        batchRequests.add(request);

        // Add the delete requests for the pseudoGroups
        for (String suffix : pseudoGroupSuffixes) {
            JSONObject psRequest = new JSONObject();
            psRequest.put(METHOD_PARAM, "POST");
            psRequest.put(CHARSET_PARAM, UTF_8);
            psRequest.put(URL_PARAM, getDeleteURI(parentGroupId + "-" + suffix));
            JSONObject psParams = new JSONObject();
            psParams.put(OPERATION_PARAM, "delete");
            psParams.put(PARAMETERS_PARAM, psParams);
            batchRequests.add(psRequest);
        }

        PostMethod method = new PostMethod(url + BATCH_URI);
        JSONArray json = JSONArray.fromObject(batchRequests);
        method.setParameter(BATCH_REQUESTS_PARAM, json.toString());
        method.setParameter(CHARSET_PARAM, UTF_8);

        try {
            if (!dryrun) {
                NakamuraHttpUtils.http(client, method);
                AuditLogUtils.audit(AuditLogUtils.GROUP_DELETED, null, parentGroupId, "deleted",
                        AuditLogUtils.SUCCESS);
                for (String suffix : pseudoGroupSuffixes) {
                    AuditLogUtils.audit(AuditLogUtils.GROUP_DELETED, null, parentGroupId + "-" + suffix, "deleted",
                            AuditLogUtils.SUCCESS);
                }
            }
        } catch (GroupModificationException e) {
            AuditLogUtils.audit(AuditLogUtils.GROUP_DELETED, null, parentGroupId, "deleted", AuditLogUtils.FAILURE);
            for (String suffix : pseudoGroupSuffixes) {
                AuditLogUtils.audit(AuditLogUtils.GROUP_DELETED, null, parentGroupId + "-" + suffix, "deleted",
                        AuditLogUtils.FAILURE);
            }
            throw e;
        }
    }

    public List<String> getRoles(String worldId) {
        List<String> roles = new ArrayList<String>();
        GetMethod get = new GetMethod(url + GROUP_PATH_PREFIX + "/" + worldId + ".json");
        try {
            JSONObject json = NakamuraHttpUtils.http(NakamuraHttpUtils.getHttpClient(url, username, password), get);
            if (json != null) {
                JSONArray jsonRoles = json.getJSONObject("properties").getJSONArray("sakai:roles");
                for (Object jRole : jsonRoles) {
                    JSONObject j = (JSONObject) jRole;
                    roles.add((String) j.get("id"));
                }
            }
        } catch (GroupModificationException gme) {
            log.error("wtf? on a get?", gme);
        }
        return roles;
    }

    /**
     * Create a pseudoGroup in Sakai OAE.
     * A pseudoGroup is an Authorizable that represents a role group for a course.
     * @param nakamuraGroupId the id of the psuedoGroup in OAE
     * @param groupName the name of the Grouper group.
     * @param description a description for the group in OAE
     * @throws GroupModificationException
     */
    protected void createPseudoGroup(String nakamuraGroupId, String groupName, String description)
            throws GroupModificationException {
        String role = nakamuraGroupId.substring(nakamuraGroupId.lastIndexOf('-') + 1);
        HttpClient client = NakamuraHttpUtils.getHttpClient(url, username, password);
        PostMethod method = new PostMethod(url + GROUP_CREATE_URI);
        method.addParameter(":name", nakamuraGroupId);
        method.addParameter(CHARSET_PARAM, UTF_8);
        method.addParameter("sakai:group-id", nakamuraGroupId);
        method.addParameter("sakai:excludeSearch", WorldConstants.TRUE);
        method.addParameter("sakai:group-description", description);
        method.addParameter("sakai:group-title", nakamuraGroupId + "(" + role + ")");
        method.addParameter("sakai:pseudoGroup", WorldConstants.TRUE);
        method.addParameter("sakai:pseudogroupparent", groupIdAdapter.getWorldId(nakamuraGroupId));
        method.setParameter("sakai:group-joinable", WorldConstants.NO);
        method.addParameter(GROUPER_NAME_PROP, groupName.substring(0, groupName.lastIndexOf(":") + 1)
                + nakamuraGroupId.substring(nakamuraGroupId.lastIndexOf("-") + 1));
        method.setParameter(GROUPER_PROVISIONED_PROP, WorldConstants.TRUE);

        try {
            if (!dryrun) {
                NakamuraHttpUtils.http(client, method);
            }
            log.info("Created pseudoGroup in OAE for " + nakamuraGroupId);
            AuditLogUtils.audit(AuditLogUtils.GROUP_CREATED, null, nakamuraGroupId, description,
                    AuditLogUtils.SUCCESS);
        } catch (GroupModificationException e) {
            AuditLogUtils.audit(AuditLogUtils.GROUP_CREATED, null, nakamuraGroupId, description,
                    AuditLogUtils.FAILURE);
            throw e;
        }
    }

    /**
     * Send a batch request to Sakai OAE.
     * @param requests a JSONArray for JSONObjects. Each JSONObject represents a request.
     * @throws GroupModificationException
     */
    protected void batchPost(JSONArray requests) throws GroupModificationException {
        PostMethod method = new PostMethod(url + BATCH_URI);
        method.setParameter(BATCH_REQUESTS_PARAM, requests.toString());
        method.setParameter(CHARSET_PARAM, UTF_8);
        if (!dryrun) {
            NakamuraHttpUtils.http(NakamuraHttpUtils.getHttpClient(url, username, password), method);
        }
    }

    /**
     * @param userId
     * @return does this user exist in Sakai OAE?
     */
    private boolean userExists(String userId) {
        boolean exists = false;
        if (dryrun || userExistsInSakai.containsKey(userId)) {
            exists = true;
        }
        if (!exists) {
            try {
                HttpClient client = NakamuraHttpUtils.getHttpClient(url, username, password);
                int returnCode = client.executeMethod(
                        new GetMethod(url.toString() + "/system/userManager/user/" + userId + ".json"));
                exists = (returnCode == HttpStatus.SC_OK);
            } catch (IOException ioe) {
                log.error("Could not communicate with OAE to check if a user exists.");
            }
        }
        return exists;
    }

    /**
     * @param groupId the id of the OAE group
     * @return the URI to the delete operation.
     */
    protected String getDeleteURI(String groupId) {
        return GROUP_PATH_PREFIX + "/" + groupId + ".delete.json";
    }

    /**
     * @param groupId the id of the OAE group
     * @return the URI to the update operation.
     */
    protected String getUpdateURI(String groupId) {
        return GROUP_PATH_PREFIX + "/" + groupId + ".update.json";
    }

    protected void setUrl(String urlString) {
        try {
            url = new URL(urlString);
        } catch (MalformedURLException mfe) {
            log.error("Could not parse " + urlString + "into a URL.");
            throw new RuntimeException(mfe.toString());
        }
    }
}