uk.ac.cam.cl.dtg.segue.api.UsersFacade.java Source code

Java tutorial

Introduction

Here is the source code for uk.ac.cam.cl.dtg.segue.api.UsersFacade.java

Source

/**
 * Copyright 2014 Stephen Cummins
 *
 * 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 uk.ac.cam.cl.dtg.segue.api;

import static uk.ac.cam.cl.dtg.isaac.api.Constants.SUBJECT_INTEREST;
import static uk.ac.cam.cl.dtg.segue.api.Constants.*;

import com.google.api.client.util.Maps;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.Lists;
import io.swagger.annotations.Api;

import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.*;
import java.util.concurrent.TimeUnit;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.EntityTag;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

import org.apache.commons.lang3.Validate;
import org.jboss.resteasy.annotations.GZIP;
import org.joda.time.LocalDate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import uk.ac.cam.cl.dtg.segue.api.managers.SegueResourceMisuseException;
import uk.ac.cam.cl.dtg.segue.api.managers.StatisticsManager;
import uk.ac.cam.cl.dtg.segue.api.managers.UserAssociationManager;
import uk.ac.cam.cl.dtg.segue.api.managers.UserAccountManager;
import uk.ac.cam.cl.dtg.segue.api.monitors.IMisuseMonitor;
import uk.ac.cam.cl.dtg.segue.api.monitors.PasswordResetRequestMisuseHandler;
import uk.ac.cam.cl.dtg.segue.api.monitors.SegueLoginMisuseHandler;
import uk.ac.cam.cl.dtg.segue.auth.AuthenticationProvider;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.AuthenticationProviderMappingException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.DuplicateAccountException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.FailedToHashPasswordException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.IncorrectCredentialsProvidedException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.InvalidPasswordException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.InvalidTokenException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.MissingRequiredFieldException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.NoCredentialsAvailableException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.NoUserException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.NoUserLoggedInException;
import uk.ac.cam.cl.dtg.segue.comm.CommunicationException;
import uk.ac.cam.cl.dtg.segue.comm.EmailMustBeVerifiedException;
import uk.ac.cam.cl.dtg.segue.dao.ILogManager;
import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException;
import uk.ac.cam.cl.dtg.segue.dao.schools.SchoolListReader;
import uk.ac.cam.cl.dtg.segue.dao.schools.UnableToIndexSchoolsException;
import uk.ac.cam.cl.dtg.segue.dos.AbstractEmailPreferenceManager;
import uk.ac.cam.cl.dtg.segue.dos.AbstractUserPreferenceManager;
import uk.ac.cam.cl.dtg.segue.dos.IEmailPreference;
import uk.ac.cam.cl.dtg.segue.dos.UserPreference;
import uk.ac.cam.cl.dtg.segue.dos.users.RegisteredUser;
import uk.ac.cam.cl.dtg.segue.dos.users.Role;
import uk.ac.cam.cl.dtg.segue.dos.users.School;
import uk.ac.cam.cl.dtg.segue.dos.users.UserSettings;
import uk.ac.cam.cl.dtg.segue.dto.SegueErrorResponse;
import uk.ac.cam.cl.dtg.segue.dto.users.RegisteredUserDTO;
import uk.ac.cam.cl.dtg.segue.dto.users.UserSummaryDTO;
import uk.ac.cam.cl.dtg.segue.search.SegueSearchException;
import uk.ac.cam.cl.dtg.util.PropertiesLoader;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.api.client.util.Sets;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Inject;

/**
 * User facade.
 *
 * @author Stephen Cummins
 *
 */
@Path("/")
@Api(value = "/users")
public class UsersFacade extends AbstractSegueFacade {
    private static final Logger log = LoggerFactory.getLogger(UsersFacade.class);
    private final UserAccountManager userManager;
    private final StatisticsManager statsManager;
    private final UserAssociationManager userAssociationManager;
    private final IMisuseMonitor misuseMonitor;
    private final AbstractEmailPreferenceManager emailPreferenceManager;
    private final AbstractUserPreferenceManager userPreferenceManager;
    private final SchoolListReader schoolListReader;
    private final Supplier<Set<School>> schoolOtherSupplier;

    /**
     * Construct an instance of the UsersFacade.
     *
     * @param properties
     *            - properties loader for the application
     * @param userManager
     *            - user manager for the application
     * @param logManager
     *            - so we can log interesting events.
     * @param statsManager
     *            - so we can view stats on interesting events.
     * @param userAssociationManager
     *            - so we can check permissions..
     * @param misuseMonitor
     *            - so we can check for misuse
     * @param emailPreferenceManager
     *            - so we can provide email preferences
     */
    @Inject
    public UsersFacade(final PropertiesLoader properties, final UserAccountManager userManager,
            final ILogManager logManager, final StatisticsManager statsManager,
            final UserAssociationManager userAssociationManager, final IMisuseMonitor misuseMonitor,
            final AbstractEmailPreferenceManager emailPreferenceManager,
            final AbstractUserPreferenceManager userPreferenceManager, final SchoolListReader schoolListReader) {
        super(properties, logManager);
        this.userManager = userManager;
        this.statsManager = statsManager;
        this.userAssociationManager = userAssociationManager;
        this.misuseMonitor = misuseMonitor;
        this.emailPreferenceManager = emailPreferenceManager;
        this.userPreferenceManager = userPreferenceManager;
        this.schoolListReader = schoolListReader;

        this.schoolOtherSupplier = Suppliers.memoizeWithExpiration(new Supplier<Set<School>>() {
            @Override
            public Set<School> get() {
                try {
                    List<RegisteredUserDTO> users = userManager.findUsers(new RegisteredUserDTO());

                    Set<School> schoolOthers = Sets.newHashSet();

                    for (RegisteredUserDTO user : users) {
                        if (user.getSchoolOther() != null) {
                            School pseudoSchool = new School();
                            pseudoSchool.setUrn(Integer.toString(user.getSchoolOther().hashCode()));
                            pseudoSchool.setName(user.getSchoolOther());
                            pseudoSchool.setDataSource(School.SchoolDataSource.USER_ENTERED);
                            schoolOthers.add(pseudoSchool);
                        }
                    }
                    return schoolOthers;
                } catch (SegueDatabaseException e) {
                    return null;
                }
            }
        }, 1, TimeUnit.DAYS);
    }

    /**
     * Get the details of the currently logged in user.
     *
     * @param request
     *            - request information used for caching.
     * @param httpServletRequest
     *            - the request which may contain session information.
     * @return Returns the current user DTO if we can get it or null response if we can't. It will be a 204 No Content
     */
    @GET
    @Path("users/current_user")
    @Produces(MediaType.APPLICATION_JSON)
    @GZIP
    public Response getCurrentUserEndpoint(@Context final Request request,
            @Context final HttpServletRequest httpServletRequest) {
        try {
            RegisteredUserDTO currentUser = userManager.getCurrentRegisteredUser(httpServletRequest);

            // Calculate the ETag based on User we just retrieved from the DB
            EntityTag etag = new EntityTag("currentUser".hashCode() + currentUser.toString().hashCode() + "");
            Response cachedResponse = generateCachedResponse(request, etag,
                    Constants.NEVER_CACHE_WITHOUT_ETAG_CHECK);
            if (cachedResponse != null) {
                return cachedResponse;
            }

            return Response.ok(currentUser).tag(etag)
                    .cacheControl(getCacheControl(Constants.NEVER_CACHE_WITHOUT_ETAG_CHECK, false)).build();
        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        }
    }

    /**
     * This method allows users to create a local account or update their settings.
     *
     * It will also allow administrators to change any user settings.
     *
     * @param request
     *            - the http request of the user wishing to authenticate
     * @param response
     *            to tell the browser to store the session in our own segue cookie.
     * @param userObjectString
     *            - object containing all user account information including passwords.
     * @return the updated users object.
     */
    @POST
    @Path("users")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    @GZIP
    public Response createOrUpdateUserSettings(@Context final HttpServletRequest request,
            @Context final HttpServletResponse response, final String userObjectString) {

        UserSettings userSettingsObjectFromClient;
        try {
            ObjectMapper tmpObjectMapper = new ObjectMapper();
            tmpObjectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            userSettingsObjectFromClient = tmpObjectMapper.readValue(userObjectString, UserSettings.class);

            if (null == userSettingsObjectFromClient) {
                return new SegueErrorResponse(Status.BAD_REQUEST, "No user settings provided.").toResponse();
            }
        } catch (IOException e) {
            return new SegueErrorResponse(Status.BAD_REQUEST, "Unable to parse the user object you provided.", e)
                    .toResponse();
        }

        RegisteredUser registeredUser = userSettingsObjectFromClient.getRegisteredUser();

        Map<String, Boolean> emailPreferences = userSettingsObjectFromClient.getEmailPreferences();
        Map<String, Boolean> subjectInterests = userSettingsObjectFromClient.getSubjectInterests();

        if (null != registeredUser.getId()) {

            // Update email preferences & subject interests within the same request
            List<IEmailPreference> userEmailPreferences = emailPreferenceManager
                    .mapToEmailPreferenceList(registeredUser.getId(), emailPreferences);

            try {
                return this.updateUserObject(request, response, registeredUser,
                        userSettingsObjectFromClient.getPasswordCurrent(), userEmailPreferences, subjectInterests);
            } catch (IncorrectCredentialsProvidedException e) {
                return new SegueErrorResponse(Status.BAD_REQUEST, "Incorrect credentials provided.", e)
                        .toResponse();
            } catch (NoCredentialsAvailableException e) {
                return new SegueErrorResponse(Status.BAD_REQUEST, "No credentials available.", e).toResponse();
            }
        } else {
            return this.createUserObjectAndLogIn(request, response, registeredUser, emailPreferences);
        }

    }

    /**
     * An endpoint to be refactored out of this class, which provides access to subject interests.
     * @param httpServletRequest - the request, to work ou the current user
     * @return subject interest map
     */
    @GET
    @Path("users/subject_interests")
    @Produces(MediaType.APPLICATION_JSON)
    @GZIP
    public Response getSubjectInterests(@Context final HttpServletRequest httpServletRequest) {
        // FIXME - this endpoint does not belong here, and should probably return actual user preferences eventually.
        try {
            RegisteredUserDTO currentUser = userManager.getCurrentRegisteredUser(httpServletRequest);
            List<UserPreference> subjectPreferences = userPreferenceManager.getUserPreferences(SUBJECT_INTEREST,
                    currentUser.getId());

            Map<String, Boolean> subjectInterests = Maps.newHashMap();

            for (UserPreference preference : subjectPreferences) {
                subjectInterests.put(preference.getPreferenceName(), preference.getPreferenceValue());
            }

            return Response.ok(subjectInterests).build();
        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        } catch (SegueDatabaseException e) {
            SegueErrorResponse error = new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR,
                    "Can't load user preferences!", e);
            log.error(error.getErrorMessage(), e);
            return error.toResponse();
        }
    }

    /**
     * End point that allows a local user to generate a password reset request.
     *
     * Step 1 of password reset process - send user an e-mail
     *
     * @param userObject
     *            - A user object containing the email of the user requesting a reset
     * @param request
     *            - For logging purposes.
     * @return a successful response regardless of whether the email exists or an error code if there is a technical
     *         fault
     */
    @POST
    @Path("users/resetpassword")
    @Consumes(MediaType.APPLICATION_JSON)
    @GZIP
    public Response generatePasswordResetToken(final RegisteredUserDTO userObject,
            @Context final HttpServletRequest request) {
        if (null == userObject) {
            log.debug("User is null");
            return new SegueErrorResponse(Status.BAD_REQUEST, "No user settings provided.").toResponse();
        }

        try {
            misuseMonitor.notifyEvent(userObject.getEmail(), PasswordResetRequestMisuseHandler.class.toString());
            userManager.resetPasswordRequest(userObject);

            this.getLogManager().logEvent(userManager.getCurrentUser(request), request,
                    PASSWORD_RESET_REQUEST_RECEIVED,
                    ImmutableMap.of(LOCAL_AUTH_EMAIL_FIELDNAME, userObject.getEmail()));

            return Response.ok().build();
        } catch (NoUserException e) {
            log.warn("Password reset requested for account that does not exist: (" + userObject.getEmail() + ")");
            // Return OK so we don't leak account existence.
            return Response.ok().build();
        } catch (CommunicationException e) {
            SegueErrorResponse error = new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR,
                    "Error sending reset message.", e);
            log.error(error.getErrorMessage(), e);
            return error.toResponse();
        } catch (SegueDatabaseException | InvalidKeySpecException | NoSuchAlgorithmException e) {
            SegueErrorResponse error = new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR,
                    "Error generating password reset token.", e);
            log.error(error.getErrorMessage(), e);
            return error.toResponse();
        } catch (SegueResourceMisuseException e) {
            String message = "You have exceeded the number of requests allowed for this endpoint. "
                    + "Please try again later.";
            log.error("Too many password resets requested by: (" + userObject.getEmail() + ")", e.toString());
            return SegueErrorResponse.getRateThrottledResponse(message);
        }
    }

    /**
     * End point that verifies whether or not a password reset token is valid.
     *
     * Optional Step 2 - validate token is correct
     *
     * @param token
     *            - A password reset token
     * @return Success if the token is valid, otherwise returns not found
     */
    @GET
    @Path("users/resetpassword/{token}")
    @Produces(MediaType.APPLICATION_JSON)
    @GZIP
    public Response validatePasswordResetRequest(@PathParam("token") final String token) {
        try {
            if (userManager.validatePasswordResetToken(token)) {
                return Response.ok().build();
            }
        } catch (SegueDatabaseException e) {
            log.error("Internal database error, while validating Password Reset Request.", e);
            SegueErrorResponse error = new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR,
                    "Database error has occurred. Unable to access token list.");
            return error.toResponse();
        }

        SegueErrorResponse error = new SegueErrorResponse(Status.NOT_FOUND, "Invalid password reset token.");
        log.debug(String.format("Invalid password reset token: %s", token));
        return error.toResponse();
    }

    /**
     * Final step of password reset process. Change password.
     *
     * @param token
     *            - A password reset token
     * @param userObject
     *            - A user object containing password information.
     * @param request
     *            - For logging purposes.
     * @return successful response.
     */
    @POST
    @Path("users/resetpassword/{token}")
    @Consumes(MediaType.APPLICATION_JSON)
    @GZIP
    public Response resetPassword(@PathParam("token") final String token, final RegisteredUser userObject,
            @Context final HttpServletRequest request) {
        try {

            RegisteredUserDTO userDTO = userManager.resetPassword(token, userObject);

            this.getLogManager().logEvent(userDTO, request, PASSWORD_RESET_REQUEST_SUCCESSFUL,
                    ImmutableMap.of(LOCAL_AUTH_EMAIL_FIELDNAME, userDTO.getEmail()));
            // we can reset the misuse monitor for incorrect logins now.
            misuseMonitor.resetMisuseCount(userDTO.getEmail().toLowerCase(),
                    SegueLoginMisuseHandler.class.toString());

        } catch (InvalidTokenException e) {
            SegueErrorResponse error = new SegueErrorResponse(Status.BAD_REQUEST, "Invalid password reset token.");
            log.error("Invalid password reset token supplied: " + token);
            return error.toResponse();
        } catch (InvalidPasswordException e) {
            SegueErrorResponse error = new SegueErrorResponse(Status.BAD_REQUEST, "No password supplied.");
            return error.toResponse();
        } catch (SegueDatabaseException e) {
            String errorMsg = "Database error has occurred during reset password process. Please try again later";
            log.error(errorMsg, e);
            SegueErrorResponse error = new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, errorMsg);
            return error.toResponse();
        }

        return Response.ok().build();
    }

    /**
     * Get the event data for a specified user.
     *
     * @param request
     *            - request information used for caching.
     * @param httpServletRequest
     *            - the request which may contain session information.
     * @param userIdOfInterest
     *            - userId of interest - currently only supports looking at own data.
     * @param fromDate
     *            - date to start search
     * @param toDate
     *            - date to end search
     * @param events
     *            - comma separated list of events of interest.
     * @param bin
     *            - Should we group data into the first day of the month? true or false.
     * @return Returns a map of eventType to Map of dates to total number of events.
     */
    @GET
    @Path("users/{user_id}/event_data/over_time")
    @Produces(MediaType.APPLICATION_JSON)
    @GZIP
    public Response getEventDataForUser(@Context final Request request,
            @Context final HttpServletRequest httpServletRequest, @PathParam("user_id") final Long userIdOfInterest,
            @QueryParam("from_date") final Long fromDate, @QueryParam("to_date") final Long toDate,
            @QueryParam("events") final String events, @QueryParam("bin_data") final Boolean bin) {
        final boolean binData;
        if (null == bin || !bin) {
            binData = false;
        } else {
            binData = true;
        }

        if (null == events) {
            return new SegueErrorResponse(Status.BAD_REQUEST, "You must specify the events you are interested in.")
                    .toResponse();
        }

        if (null == fromDate || null == toDate) {
            return new SegueErrorResponse(Status.BAD_REQUEST,
                    "You must specify the from_date and to_date you are interested in.").toResponse();
        }

        try {
            RegisteredUserDTO currentUser = userManager.getCurrentRegisteredUser(httpServletRequest);

            RegisteredUserDTO userOfInterest = userManager.getUserDTOById(userIdOfInterest);
            if (userOfInterest == null) {
                throw new NoUserException();
            }

            UserSummaryDTO userOfInterestSummaryObject = userManager.convertToUserSummaryObject(userOfInterest);

            // decide if the user is allowed to view this data.
            if (!currentUser.getId().equals(userIdOfInterest)
                    && !userAssociationManager.hasPermission(currentUser, userOfInterestSummaryObject)) {
                return SegueErrorResponse.getIncorrectRoleResponse();
            }

            Map<String, Map<LocalDate, Long>> eventLogsByDate = this.statsManager.getEventLogsByDateAndUserList(
                    Lists.newArrayList(events.split(",")), new Date(fromDate), new Date(toDate),
                    Arrays.asList(userOfInterest), binData);

            return Response.ok(eventLogsByDate).build();
        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        } catch (NoUserException e) {
            return new SegueErrorResponse(Status.BAD_REQUEST, "Unable to find user with the id provided.")
                    .toResponse();
        } catch (SegueDatabaseException e) {
            log.error("Unable to look up user event history for user " + userIdOfInterest, e);
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Error while looking up event information")
                    .toResponse();
        }
    }

    /**
     * This method allows the requester to provide a list of user ids and get back a mapping of the user
     * id to the school information. This is useful for building up tables of users school information.
     *
     * @param httpServletRequest
     *            Authentication and authorisation
     * @param userIdsQueryParam
     *            The comma seperated list of user ids to try and find schools for.
     * @return A map mapping the userId to a school if we found one for it.
     */
    @GET
    @Path("users/school_lookup")
    @Produces(MediaType.APPLICATION_JSON)
    @GZIP
    public Response getUserIdToSchoolMap(@Context final HttpServletRequest httpServletRequest,
            @QueryParam("user_ids") final String userIdsQueryParam) {
        try {
            if (!isUserStaff(this.userManager, httpServletRequest)) {
                return new SegueErrorResponse(Status.FORBIDDEN,
                        "You must be a staff member to access this endpoint.").toResponse();
            }

            if (null == userIdsQueryParam || userIdsQueryParam.isEmpty()) {
                return new SegueErrorResponse(Status.BAD_REQUEST,
                        "You must provide a comma separated list of user_ids in the query param").toResponse();
            }

            String[] userIdsAsList = userIdsQueryParam.split(",");
            List<Long> userLongIds = Lists.newArrayList();

            for (int i = 0; i < userIdsAsList.length; i++) {
                userLongIds.add(Long.parseLong(userIdsAsList[i]));
            }

            final List<RegisteredUserDTO> users = this.userManager.findUsers(userLongIds);
            final ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();
            for (RegisteredUserDTO user : users) {
                if (user.getSchoolId() != null) {
                    builder.put(user.getId().toString(), schoolListReader.findSchoolById(user.getSchoolId()));
                } else if (user.getSchoolOther() != null && !user.getSchoolOther().isEmpty()) {
                    Map<String, String> schoolOtherResult = Maps.newHashMap();
                    schoolOtherResult.put("name", user.getSchoolOther());
                    builder.put(user.getId().toString(), schoolOtherResult);
                }
            }

            return Response.ok(builder.build()).build();
        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        } catch (NumberFormatException e) {
            return new SegueErrorResponse(Status.BAD_REQUEST, "Unable to parse all parameters as integers.")
                    .toResponse();
        } catch (SegueDatabaseException e) {
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Database error while looking up users", e)
                    .toResponse();
        } catch (IOException e) {
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Database error while looking up schools",
                    e).toResponse();
        } catch (UnableToIndexSchoolsException | SegueSearchException e) {
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Database error while looking up schools",
                    e).toResponse();
        }
    }

    /**
     * Get a Set of all schools reported by users in the school other field.
     *
     * @param request
     *            for caching purposes.
     * @return list of strings.
     */
    @GET
    @Path("users/schools_other")
    @Produces(MediaType.APPLICATION_JSON)
    @GZIP
    public Response getAllSchoolOtherResponses(@Context final Request request) {

        Set<School> schoolOthers = schoolOtherSupplier.get();
        if (null != schoolOthers) {
            EntityTag etag = new EntityTag(schoolOthers.toString().hashCode() + "");
            Response cachedResponse = generateCachedResponse(request, etag,
                    Constants.NEVER_CACHE_WITHOUT_ETAG_CHECK);
            if (cachedResponse != null) {
                return cachedResponse;
            }

            return Response.ok(schoolOthers).tag(etag)
                    .cacheControl(getCacheControl(Constants.NEVER_CACHE_WITHOUT_ETAG_CHECK, false)).build();

        } else {
            log.error("Unable to get school list");
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Error while looking up schools")
                    .toResponse();
        }
    }

    /**
     * Update a user object.
     *
     * This method does all of the necessary security checks to determine who is allowed to edit what.
     *
     * @param request
     *            - so that we can identify the user
     * @param response
     *            - so we can modify the session
     * @param userObjectFromClient
     *            - the new user object from the clients perspective.
     * @param passwordCurrent
     *            - the current password, used if the password has changed
     * @param emailPreferences
     *            - the email preferences for this user
     * @param subjectInterests - the subjects interests of the user, which should be removed from this method!
     * @return the updated user object.
     * @throws NoCredentialsAvailableException
     * @throws IncorrectCredentialsProvidedException
     */
    private Response updateUserObject(final HttpServletRequest request, final HttpServletResponse response,
            final RegisteredUser userObjectFromClient, final String passwordCurrent,
            final List<IEmailPreference> emailPreferences, final Map<String, Boolean> subjectInterests)
            throws IncorrectCredentialsProvidedException, NoCredentialsAvailableException {
        Validate.notNull(userObjectFromClient.getId());

        // this is an update as the user has an id
        // security checks
        try {
            // check that the current user has permissions to change this users details.
            RegisteredUserDTO currentlyLoggedInUser = this.userManager.getCurrentRegisteredUser(request);
            if (!currentlyLoggedInUser.getId().equals(userObjectFromClient.getId())
                    && currentlyLoggedInUser.getRole() != Role.ADMIN
                    && currentlyLoggedInUser.getRole() != Role.EVENT_MANAGER) {
                return new SegueErrorResponse(Status.FORBIDDEN, "You cannot change someone else's user settings.")
                        .toResponse();
            }

            // check if they are trying to change a password
            if (userObjectFromClient.getPassword() != null && !userObjectFromClient.getPassword().isEmpty()) {
                // only admins and the account owner can change passwords 
                if (!currentlyLoggedInUser.getId().equals(userObjectFromClient.getId())
                        && currentlyLoggedInUser.getRole() != Role.ADMIN) {
                    return new SegueErrorResponse(Status.FORBIDDEN, "You cannot change someone else's password.")
                            .toResponse();
                }

                // Password change requires auth check unless admin is modifying non-admin user account
                if (!(currentlyLoggedInUser.getRole() == Role.ADMIN
                        && userObjectFromClient.getRole() != Role.ADMIN)) {
                    // authenticate the user to check they are allowed to change the password
                    this.userManager.ensureCorrectPassword(AuthenticationProvider.SEGUE.name(),
                            userObjectFromClient.getEmail(), passwordCurrent);
                }
            }

            // check that any changes to protected fields being made are allowed.
            RegisteredUserDTO existingUserFromDb = this.userManager.getUserDTOById(userObjectFromClient.getId());

            if (Role.EVENT_MANAGER.equals(currentlyLoggedInUser.getRole())) {
                if (Role.ADMIN.equals(existingUserFromDb.getRole())
                        || Role.ADMIN.equals(userObjectFromClient.getRole())) {
                    return new SegueErrorResponse(Status.FORBIDDEN, "You cannot modify admin roles.").toResponse();
                }
            }

            // check that the user is allowed to change the role of another user
            // if that is what they are doing.
            if ((currentlyLoggedInUser.getRole() != Role.ADMIN
                    && currentlyLoggedInUser.getRole() != Role.EVENT_MANAGER)
                    && userObjectFromClient.getRole() != null
                    && !userObjectFromClient.getRole().equals(existingUserFromDb.getRole())) {
                return new SegueErrorResponse(Status.FORBIDDEN,
                        "You do not have permission to change a users role.").toResponse();
            }

            RegisteredUserDTO updatedUser = userManager.updateUserObject(userObjectFromClient);

            // If the user's role has changed, record it. Check this using Objects.equals() to be null safe!
            if (!Objects.equals(updatedUser.getRole(), existingUserFromDb.getRole())) {
                log.info("ADMIN user " + currentlyLoggedInUser.getEmail() + " has modified the role of "
                        + updatedUser.getEmail() + "[" + updatedUser.getId() + "]" + " to "
                        + updatedUser.getRole());
                this.getLogManager().logEvent(currentlyLoggedInUser, request, Constants.CHANGE_USER_ROLE,
                        ImmutableMap.of(USER_ID_FKEY_FIELDNAME, updatedUser.getId(), "oldRole",
                                existingUserFromDb.getRole(), "newRole", updatedUser.getRole()));
            }

            // If the user's school has changed, record it. Check this using Objects.equals() to be null safe!
            if (!Objects.equals(updatedUser.getSchoolId(), existingUserFromDb.getSchoolId())
                    || !Objects.equals(updatedUser.getSchoolOther(), existingUserFromDb.getSchoolOther())) {
                LinkedHashMap<String, Object> eventDetails = new LinkedHashMap<>();
                eventDetails.put("oldSchoolId", existingUserFromDb.getSchoolId());
                eventDetails.put("newSchoolId", updatedUser.getSchoolId());
                eventDetails.put("oldSchoolOther", existingUserFromDb.getSchoolOther());
                eventDetails.put("newSchoolOther", updatedUser.getSchoolOther());

                if (!Objects.equals(currentlyLoggedInUser.getId(), updatedUser.getId())) {
                    // This is an ADMIN user changing another user's school:
                    eventDetails.put(USER_ID_FKEY_FIELDNAME, updatedUser.getId());
                    this.getLogManager().logEvent(currentlyLoggedInUser, request,
                            Constants.ADMIN_CHANGE_USER_SCHOOL, eventDetails);
                } else {
                    this.getLogManager().logEvent(currentlyLoggedInUser, request, Constants.USER_SCHOOL_CHANGE,
                            eventDetails);
                }
            }

            // Now update the email preferences
            emailPreferenceManager.saveEmailPreferences(userObjectFromClient.getId(), emailPreferences);

            //----------------------------------------------------------------------------------------------------------
            // FIXME - the code between the dashed lines should be refactored out of this class; it does not belong here!
            // Finally update the subject interests:
            if (subjectInterests != null) {
                try {
                    List<UserPreference> userPreferences = Lists.newArrayList();
                    List<String> acceptedSubjects = Arrays.asList("PHYSICS_UNI", "PHYSICS_ALEVEL", "PHYSICS_GCSE",
                            "CHEMISTRY_UNI", "CHEMISTRY_ALEVEL", "CHEMISTRY_GCSE", "MATHS_UNI", "MATHS_ALEVEL",
                            "MATHS_GCSE");
                    for (String subject : subjectInterests.keySet()) {
                        // Validate that what is being saved is in fact acceptable:
                        if (!acceptedSubjects.contains(subject)) {
                            return new SegueErrorResponse(Status.BAD_REQUEST, "Invalid user preferences provided.")
                                    .toResponse();
                        }
                        userPreferences.add(new UserPreference(updatedUser.getId(), SUBJECT_INTEREST, subject,
                                subjectInterests.get(subject)));
                    }
                    userPreferenceManager.saveUserPreferences(userPreferences);
                } catch (SegueDatabaseException e) {
                    return new SegueErrorResponse(Status.BAD_REQUEST, "Invalid user preferences provided.")
                            .toResponse();
                }
            }
            //----------------------------------------------------------------------------------------------------------

            return Response.ok(updatedUser).build();
        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        } catch (NoUserException e) {
            return new SegueErrorResponse(Status.NOT_FOUND, "The user specified does not exist.").toResponse();
        } catch (DuplicateAccountException e) {
            return new SegueErrorResponse(Status.BAD_REQUEST,
                    "An account already exists with the e-mail address specified.").toResponse();
        } catch (SegueDatabaseException e) {
            log.error("Unable to modify user", e);
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Error while modifying the user")
                    .toResponse();
        } catch (InvalidPasswordException e) {
            return new SegueErrorResponse(Status.BAD_REQUEST, e.getMessage()).toResponse();
        } catch (MissingRequiredFieldException e) {
            log.warn("Missing field during update operation. ", e.getMessage());
            return new SegueErrorResponse(Status.BAD_REQUEST,
                    "You are missing a required field. "
                            + "Please make sure you have specified all mandatory fields in your response.")
                                    .toResponse();
        } catch (AuthenticationProviderMappingException e) {
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR,
                    "Unable to map to a known authenticator. The provider: is unknown").toResponse();
        }
    }

    /**
     * Create a user object. This method allows new user objects to be created.
     *
     * @param request
     *            - so that we can identify the user
     * @param response
     *            to tell the browser to store the session in our own segue cookie.
     * @param userObjectFromClient
     *            - the new user object from the clients perspective.
     * @param emailPreferences
     *            - the new email preferences for this user
     * @return the updated user object.
     */
    private Response createUserObjectAndLogIn(final HttpServletRequest request, final HttpServletResponse response,
            final RegisteredUser userObjectFromClient, final Map<String, Boolean> emailPreferences) {
        try {
            RegisteredUserDTO savedUser = userManager.createUserObjectAndSession(request, response,
                    userObjectFromClient);

            List<IEmailPreference> userEmailPreferences = emailPreferenceManager
                    .mapToEmailPreferenceList(savedUser.getId(), emailPreferences);

            //Now update the email preferences
            emailPreferenceManager.saveEmailPreferences(savedUser.getId(), userEmailPreferences);

            return Response.ok(savedUser).build();
        } catch (InvalidPasswordException e) {
            return new SegueErrorResponse(Status.BAD_REQUEST, e.getMessage()).toResponse();
        } catch (FailedToHashPasswordException e) {
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Unable to set a password.").toResponse();
        } catch (MissingRequiredFieldException e) {
            log.warn("Missing field during update operation. ", e);
            return new SegueErrorResponse(Status.BAD_REQUEST,
                    "You are missing a required field. "
                            + "Please make sure you have specified all mandatory fields in your response.")
                                    .toResponse();
        } catch (DuplicateAccountException e) {
            return new SegueErrorResponse(Status.BAD_REQUEST,
                    "An account already exists with the e-mail address specified.").toResponse();
        } catch (SegueDatabaseException e) {
            String errorMsg = "Unable to set a password, due to an internal database error.";
            log.error(errorMsg, e);
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, errorMsg).toResponse();
        } catch (AuthenticationProviderMappingException e) {
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR,
                    "Unable to map to a known authenticator. The provider: is unknown").toResponse();
        } catch (EmailMustBeVerifiedException e) {
            return new SegueErrorResponse(Status.BAD_REQUEST, "You cannot register with an Isaac email address.")
                    .toResponse();
        }
    }
}