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

Java tutorial

Introduction

Here is the source code for uk.ac.cam.cl.dtg.segue.api.AdminFacade.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 com.google.common.collect.Lists;
import com.google.inject.name.Named;
import io.swagger.annotations.Api;

import com.opencsv.CSVWriter;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.StringWriter;
import java.io.IOException;

import java.util.List;
import java.util.Map;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.ArrayList;

import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
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.BadRequestException;
import javax.ws.rs.ForbiddenException;
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.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.jboss.resteasy.annotations.GZIP;
import org.joda.time.LocalDate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.google.api.client.util.Maps;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.inject.Inject;

import uk.ac.cam.cl.dtg.segue.api.Constants.EnvironmentType;
import uk.ac.cam.cl.dtg.segue.api.managers.StatisticsManager;
import uk.ac.cam.cl.dtg.segue.api.managers.UserAccountManager;
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.dao.ILogManager;
import uk.ac.cam.cl.dtg.segue.dao.LocationManager;
import uk.ac.cam.cl.dtg.segue.dao.ResourceNotFoundException;
import uk.ac.cam.cl.dtg.segue.dao.SegueDatabaseException;
import uk.ac.cam.cl.dtg.segue.dao.content.ContentManagerException;
import uk.ac.cam.cl.dtg.segue.dao.content.IContentManager;
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.AbstractUserPreferenceManager;
import uk.ac.cam.cl.dtg.segue.dos.UserPreference;
import uk.ac.cam.cl.dtg.segue.dos.content.Content;
import uk.ac.cam.cl.dtg.segue.dos.users.EmailVerificationStatus;
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.dto.ResultsWrapper;
import uk.ac.cam.cl.dtg.segue.dto.SegueErrorResponse;
import uk.ac.cam.cl.dtg.segue.dto.content.ContentDTO;
import uk.ac.cam.cl.dtg.segue.dto.content.ContentSummaryDTO;
import uk.ac.cam.cl.dtg.segue.dto.users.AbstractSegueUserDTO;
import uk.ac.cam.cl.dtg.segue.dto.users.RegisteredUserDTO;
import uk.ac.cam.cl.dtg.segue.etl.GithubPushEventPayload;
import uk.ac.cam.cl.dtg.segue.search.SegueSearchException;
import uk.ac.cam.cl.dtg.util.PropertiesLoader;
import uk.ac.cam.cl.dtg.util.locations.Location;
import uk.ac.cam.cl.dtg.util.locations.LocationServerException;
import uk.ac.cam.cl.dtg.util.locations.PostCodeRadius;

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

/**
 * Admin facade for segue.
 * 
 * @author Stephen Cummins
 * 
 */
@Path("/admin")
@Api(value = "/admin")
public class AdminFacade extends AbstractSegueFacade {
    private static final Logger log = LoggerFactory.getLogger(AdminFacade.class);

    private final UserAccountManager userManager;
    private final IContentManager contentManager;
    private final String contentIndex;

    private final StatisticsManager statsManager;

    private final LocationManager locationManager;

    private final SchoolListReader schoolReader;

    private final AbstractUserPreferenceManager userPreferenceManager;

    /**
     * Create an instance of the administrators facade.
     * 
     * @param properties
     *            - the fully configured properties loader for the api.
     * @param userManager
     *            - The manager object responsible for users.
     * @param contentManager
     *            - The content manager used by the api.
     * @param logManager
     *            - So we can log events of interest.
     * @param statsManager
     *            - So we can report high level stats.
     * @param locationManager
     *            - for geocoding if we need it.
     * @param schoolReader
     *            - for looking up school information
     */
    @Inject
    public AdminFacade(final PropertiesLoader properties, final UserAccountManager userManager,
            final IContentManager contentManager, @Named(CONTENT_INDEX) final String contentIndex,
            final ILogManager logManager, final StatisticsManager statsManager,
            final LocationManager locationManager, final SchoolListReader schoolReader,
            final AbstractUserPreferenceManager userPreferenceManager) {
        super(properties, logManager);
        this.userManager = userManager;
        this.contentManager = contentManager;
        this.contentIndex = contentIndex;
        this.statsManager = statsManager;
        this.locationManager = locationManager;
        this.schoolReader = schoolReader;
        this.userPreferenceManager = userPreferenceManager;
    }

    /**
     * Statistics endpoint.
     * 
     * @param request
     *            - to determine access.
     * @return stats
     */
    @GET
    @Path("/stats/")
    @Produces(MediaType.APPLICATION_JSON)
    @GZIP
    public Response getStatistics(@Context final HttpServletRequest request) {
        try {
            if (!isUserStaff(request)) {
                return new SegueErrorResponse(Status.FORBIDDEN, "You must be an admin to access this endpoint.")
                        .toResponse();
            }

            return Response.ok(statsManager.outputGeneralStatistics())
                    .cacheControl(getCacheControl(NUMBER_SECONDS_IN_FIVE_MINUTES, false)).build();
        } catch (SegueDatabaseException e) {
            log.error("Unable to load general statistics.", e);
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Database error", e).toResponse();
        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        }
    }

    /**
     * Locations stats.
     * 
     * @param request
     *            - to determine access.
     * @param requestForCaching
     *            - to speed up access.
     * @param fromDate
     *            - date to start search
     * @param toDate
     *            - date to end search
     * @return stats
     */
    @GET
    @Path("/stats/users/last_locations")
    @Produces(MediaType.APPLICATION_JSON)
    @GZIP
    public Response getLastLocations(@Context final HttpServletRequest request,
            @Context final Request requestForCaching, @QueryParam("from_date") final Long fromDate,
            @QueryParam("to_date") final Long toDate) {
        try {
            if (!isUserStaff(request)) {
                return new SegueErrorResponse(Status.FORBIDDEN,
                        "You must be a staff member to access this endpoint.").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();
            }
            if (toDate < fromDate) {
                return new SegueErrorResponse(Status.BAD_REQUEST, "The from_date must be before the to_date.")
                        .toResponse();
            }

            Collection<Location> locationInformation = statsManager.getLocationInformation(new Date(fromDate),
                    new Date(toDate));

            return Response.ok(locationInformation)
                    .cacheControl(getCacheControl(NUMBER_SECONDS_IN_FIVE_MINUTES, false)).build();
        } catch (SegueDatabaseException e) {
            log.error("Database error while trying to get last locations", e);
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Database error", e).toResponse();
        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        }
    }

    /**
     * Statistics endpoint.
     * 
     * @param request
     *            - to determine access.
     * @param requestForCache
     *            - to determine caching.
     * @return stats
     */
    @GET
    @Path("/stats/schools")
    @Produces(MediaType.APPLICATION_JSON)
    @GZIP
    public Response getSchoolsStatistics(@Context final HttpServletRequest request,
            @Context final Request requestForCache) {
        try {
            if (!isUserStaff(request)) {
                return new SegueErrorResponse(Status.FORBIDDEN,
                        "You must be an admin user to access this endpoint.").toResponse();
            }

            List<Map<String, Object>> schoolStatistics = statsManager.getSchoolStatistics();

            // Calculate the ETag
            EntityTag etag = new EntityTag(schoolStatistics.toString().hashCode() + "");

            Response cachedResponse = generateCachedResponse(requestForCache, etag);
            if (cachedResponse != null) {
                return cachedResponse;
            }

            return Response.ok(schoolStatistics).tag(etag)
                    .cacheControl(getCacheControl(NUMBER_SECONDS_IN_FIVE_MINUTES, false)).build();
        } catch (UnableToIndexSchoolsException e) {
            log.error("Unable to get school statistics", e);
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR,
                    "Unable To Index SchoolIndexer Exception in admin facade", e).toResponse();
        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        } catch (SegueDatabaseException | SegueSearchException e1) {
            log.error("Unable to get school statistics", e1);
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Database error during user lookup")
                    .toResponse();
        }
    }

    /**
     * Get user last seen information map.
     * 
     * @param request
     *            - to determine access.
     * @return stats
     */
    @GET
    @Path("/stats/users/last_access")
    @Produces(MediaType.APPLICATION_JSON)
    @GZIP
    public Response getUserLastAccessInformation(@Context final HttpServletRequest request) {
        try {
            if (!isUserAnAdmin(request)) {
                return new SegueErrorResponse(Status.FORBIDDEN,
                        "You must be an admin user to access this endpoint.").toResponse();
            }

            return Response.ok(statsManager.getLastSeenUserMap()).build();
        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        }
    }

    /**
     * This method will allow users to be mass-converted to a new role.
     * 
     * @param request
     *            - to help determine access rights.
     * @param role
     *            - new role.
     * @param userIds
     *            - a list of user ids to change en-mass
     * @return Success shown by returning an ok response
     */
    @POST
    @Path("/users/change_role/{role}")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public synchronized Response modifyUsersRole(@Context final HttpServletRequest request,
            @PathParam("role") final String role, final List<Long> userIds) {
        try {
            if (!isUserAnAdminOrEventManager(request)) {
                return new SegueErrorResponse(Status.FORBIDDEN, "You must be staff to access this endpoint.")
                        .toResponse();
            }

            Role requestedRole = Role.valueOf(role);
            RegisteredUserDTO requestingUser = userManager.getCurrentRegisteredUser(request);

            if (userIds.contains(requestingUser.getId())) {
                return new SegueErrorResponse(Status.FORBIDDEN, "Aborted - you cannoted modify your own role.")
                        .toResponse();
            }

            // can't promote anyone to a role higher than yourself
            if (requestedRole.ordinal() >= requestingUser.getRole().ordinal()) {
                return new SegueErrorResponse(Status.FORBIDDEN,
                        "Cannot change to role equal or higher than your own.").toResponse();
            }

            // fail fast - break if any of the users given already have the role they are being elevated to
            for (Long userid : userIds) {
                RegisteredUserDTO user = this.userManager.getUserDTOById(userid);

                if (null == user) {
                    throw new NoUserException();
                }

                // if a user already has this role, abort
                if (user.getRole() != null && user.getRole() == requestedRole) {
                    return new SegueErrorResponse(Status.BAD_REQUEST, "Aborted - cannot demote one or more users "
                            + "who have roles equal or higher than new role").toResponse();
                }

                // if a user has a higher role than the requester, abort
                if (user.getRole() != null && user.getRole().ordinal() >= requestingUser.getRole().ordinal()) {
                    return new SegueErrorResponse(Status.FORBIDDEN, "Aborted - cannot demote one or more users "
                            + "who have roles equal or higher than you,").toResponse();
                }
            }

            for (Long userid : userIds) {
                RegisteredUserDTO user = this.userManager.getUserDTOById(userid);
                Role oldRole = user.getRole();
                this.userManager.updateUserRole(userid, requestedRole);
                this.getLogManager().logEvent(requestingUser, request, Constants.CHANGE_USER_ROLE, ImmutableMap
                        .of(USER_ID_FKEY_FIELDNAME, user.getId(), "oldRole", oldRole, "newRole", requestedRole));
            }

        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        } catch (NoUserException e) {
            log.error("NoUserException when attempting to demote users.", e);
            return new SegueErrorResponse(Status.BAD_REQUEST, "One or more users could not be found").toResponse();
        } catch (SegueDatabaseException e) {
            log.error("Database error while trying to change user role", e);
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Could not save new role to the database")
                    .toResponse();
        }

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

    /**
     * This method will allow users' email verification status to be changed en-mass.
     * 
     * @param request
     *            - to help determine access rights.
     * @param emailVerificationStatus
     *            - new emailVerificationStatus.
     * @param emails
     *            - a list of user emails that need to be changed
     * @param checkEmailsExistBeforeApplying
     *            - tells us whether to check whether all emails exist before applying
     * @return Success shown by returning an ok response
     */
    @POST
    @Path("/users/change_email_verification_status/{emailVerificationStatus}/{checkEmailsExistBeforeApplying}")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public synchronized Response modifyUsersEmailVerificationStatus(@Context final HttpServletRequest request,
            @PathParam("emailVerificationStatus") final String emailVerificationStatus,
            @PathParam("checkEmailsExistBeforeApplying") final boolean checkEmailsExistBeforeApplying,
            final List<String> emails) {
        try {
            if (!isUserAnAdminOrEventManager(request)) {
                return new SegueErrorResponse(Status.FORBIDDEN, "You must be staff to access this endpoint.")
                        .toResponse();
            }

            EmailVerificationStatus requestedEmailVerificationStatus = EmailVerificationStatus
                    .valueOf(emailVerificationStatus);
            RegisteredUserDTO requestingUser = userManager.getCurrentRegisteredUser(request);

            if (emails.contains(requestingUser.getEmail())) {
                return new SegueErrorResponse(Status.FORBIDDEN, "Aborted - you cannot modify yourself.")
                        .toResponse();
            }

            if (checkEmailsExistBeforeApplying) {
                // fail fast - break if any of the users given already have the role they are being elevated to
                for (String email : emails) {
                    RegisteredUserDTO user = this.userManager.getUserDTOByEmail(email);

                    if (null == user) {
                        log.error(String.format("No user could be found with email (%s)", email));
                        throw new NoUserException();
                    }
                }
            }

            for (String email : emails) {
                this.userManager.updateUserEmailVerificationStatus(email, requestedEmailVerificationStatus);
            }

        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        } catch (NoUserException e) {
            log.error("NoUserException when attempting to change users verification status.", e);
            return new SegueErrorResponse(Status.BAD_REQUEST, "One or more users could not be found").toResponse();
        } catch (SegueDatabaseException e) {
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR,
                    "Could not save new email verification status to the database").toResponse();
        }

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

    /**
     * This method will delete all cached data from the CMS and any search indices.
     * 
     * @param request
     *            - containing user session information.
     * 
     * @return the latest version id that will be cached if content is requested.
     */
    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/reload_properties")
    public synchronized Response reloadProperties(@Context final HttpServletRequest request) {
        try {
            if (isUserAnAdmin(request)) {
                log.info("Triggering properties reload ...");
                this.getProperties().triggerPropertiesRefresh();

                ImmutableMap<String, String> response = new ImmutableMap.Builder<String, String>()
                        .put("result", "success").build();

                return Response.ok(response).build();
            } else {
                return new SegueErrorResponse(Status.FORBIDDEN,
                        "You must be an administrator to use this function.").toResponse();
            }

        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        } catch (IOException e) {
            log.error("Unable to trigger property refresh", e);
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Unable to trigger properties refresh", e)
                    .toResponse();
        }
    }

    /**
     * Rest end point to allow content editors to see the content which failed to import into segue.
     * 
     * @param request
     *            - to identify if the user is authorised.
     * @param requestForCaching
     *            - to determine if the content is still fresh..
     * @return a content object, such that the content object has children. The children represent each source file in
     *         error and the grand children represent each error.
     */
    @SuppressWarnings("unchecked")
    @GET
    @Path("/content_problems")
    @Produces(MediaType.APPLICATION_JSON)
    @GZIP
    public Response getContentProblems(@Context final HttpServletRequest request,
            @Context final Request requestForCaching) {
        Map<Content, List<String>> problemMap = this.contentManager.getProblemMap(this.contentIndex);

        if (this.getProperties().getProperty(Constants.SEGUE_APP_ENVIRONMENT).equals(EnvironmentType.PROD.name())) {
            try {
                if (!isUserStaff(request)) {
                    return new SegueErrorResponse(Status.FORBIDDEN, "You must be an admin to access this endpoint.")
                            .toResponse();
                }
            } catch (NoUserLoggedInException e) {
                return SegueErrorResponse.getNotLoggedInResponse();
            }
        }

        // Calculate the ETag
        EntityTag etag = new EntityTag(this.contentManager.getCurrentContentSHA().hashCode() + "");

        Response cachedResponse = generateCachedResponse(requestForCaching, etag, NEVER_CACHE_WITHOUT_ETAG_CHECK);
        if (cachedResponse != null) {
            return cachedResponse;
        }

        if (null == problemMap) {
            return Response.ok(Maps.newHashMap()).build();
        }

        // build up a content object to return.
        int errors = 0;
        int failures = 0;
        Builder<String, Object> responseBuilder = ImmutableMap.builder();
        List<Map<String, Object>> errorList = Lists.newArrayList();

        Map<String, Map<String, Object>> lookupMap = Maps.newHashMap();

        // go through each errored content and list of errors
        for (Map.Entry<Content, List<String>> pair : problemMap.entrySet()) {
            Map<String, Object> errorRecord = Maps.newHashMap();

            Content partialContentWithErrors = pair.getKey();

            errorRecord.put("partialContent", partialContentWithErrors);

            errorRecord.put("successfulIngest", false);
            failures++;

            if (partialContentWithErrors.getId() != null) {
                try {

                    boolean success = this.contentManager.getContentById(this.contentManager.getCurrentContentSHA(),
                            partialContentWithErrors.getId()) != null;

                    errorRecord.put("successfulIngest", success);
                    if (success) {
                        failures--;
                    }

                } catch (ContentManagerException e) {
                    e.printStackTrace();
                }
            }

            List<String> listOfErrors = Lists.newArrayList();
            for (String s : pair.getValue()) {
                listOfErrors.add(s);
                // special case when duplicate ids allow one in.
                if (s.toLowerCase().contains("index failure") && errorRecord.get("successfulIngest").equals(true)) {
                    errorRecord.put("successfulIngest", false);
                    failures++;
                }
                errors++;
            }

            errorRecord.put("listOfErrors", listOfErrors);
            // we only want one error record per canonical path so batch them together if we have seen it before.
            if (lookupMap.containsKey(partialContentWithErrors.getCanonicalSourceFile())) {
                Map<String, Object> existingErrorRecord = lookupMap
                        .get(partialContentWithErrors.getCanonicalSourceFile());

                if (existingErrorRecord.get("successfulIngest").equals(false)
                        || errorRecord.get("successfulIngest").equals(false)) {
                    existingErrorRecord.put("successfulIngest", false);
                }

                ((List<String>) existingErrorRecord.get("listOfErrors")).addAll(listOfErrors);
            } else {
                errorList.add(errorRecord);
                lookupMap.put(partialContentWithErrors.getCanonicalSourceFile(), errorRecord);
            }
        }

        responseBuilder.put("brokenFiles", lookupMap.keySet().size());
        responseBuilder.put("totalErrors", errors);
        responseBuilder.put("errorsList", errorList);
        responseBuilder.put("failedFiles", failures);
        responseBuilder.put("currentLiveVersion", this.contentManager.getCurrentContentSHA());

        return Response.ok(responseBuilder.build()).cacheControl(getCacheControl(NUMBER_SECONDS_IN_MINUTE, false))
                .tag(etag).build();
    }

    /**
     * List users by id or email.
     * 
     * @param httpServletRequest
     *            - for checking permissions
     * @param request
     *            - for caching
     * @param userId
     *            - if searching by id
     * @param email
     *            - if searching by e-mail
     * @param familyName
     *            - if searching by familyName
     * @param role
     *            - if searching by role
     * @param schoolOther
     *            - if searching by school other field.
     * @param postcode
     *            - if searching by postcode.
     * @param schoolURN
     *            - if searching by school by the URN.
     * @param subjectOfInterest
     *            - if searching by subject interest
     * @return a userDTO or a segue error response
     */
    @GET
    @Path("/users")
    @Produces(MediaType.APPLICATION_JSON)
    @GZIP
    public Response findUsers(@Context final HttpServletRequest httpServletRequest, @Context final Request request,
            @QueryParam("id") final Long userId, @QueryParam("email") @Nullable final String email,
            @QueryParam("familyName") @Nullable final String familyName,
            @QueryParam("role") @Nullable final Role role,
            @QueryParam("schoolOther") @Nullable final String schoolOther,
            @QueryParam("postcode") @Nullable final String postcode,
            @QueryParam("postcodeRadius") @Nullable final String postcodeRadius,
            @QueryParam("schoolURN") @Nullable final String schoolURN,
            @QueryParam("subjectOfInterest") @Nullable final String subjectOfInterest) {

        RegisteredUserDTO currentUser;
        try {
            currentUser = userManager.getCurrentRegisteredUser(httpServletRequest);
            if (!isUserAnAdminOrEventManager(httpServletRequest)) {
                return new SegueErrorResponse(Status.FORBIDDEN, "You are not authorised to access this function.")
                        .toResponse();
            }

            if (!currentUser.getRole().equals(Role.ADMIN) && (null != familyName) && familyName.isEmpty()
                    && (null == schoolOther) && (null != email) && email.isEmpty() && (null == schoolURN)
                    && (null == postcode)) {
                return new SegueErrorResponse(Status.FORBIDDEN,
                        "You do not have permission to do wildcard searches.").toResponse();

            }
        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        }

        try {
            RegisteredUserDTO userPrototype = new RegisteredUserDTO();
            if (null != userId) {
                userPrototype.setId(userId);
            }

            if (null != email && !email.isEmpty()) {
                if (currentUser.getRole().equals(Role.EVENT_MANAGER)
                        && email.replaceAll("[^A-z]", "").length() < 4) {
                    return new SegueErrorResponse(Status.FORBIDDEN,
                            "You do not have permission to do wildcard searches with less than 4 characters.")
                                    .toResponse();
                }
                userPrototype.setEmail(email);
            }

            if (null != familyName && !familyName.isEmpty()) {
                // Event managers aren't allowed to do short wildcard searches, but need surnames less than 4 chars too.
                if (currentUser.getRole().equals(Role.EVENT_MANAGER)
                        && (familyName.replaceAll("[^A-z]", "").length() < 4)
                        && (familyName.length() != familyName.replaceAll("[^A-z]", "").length())) {
                    return new SegueErrorResponse(Status.FORBIDDEN,
                            "You do not have permission to do wildcard searches with less than 4 characters.")
                                    .toResponse();
                }
                userPrototype.setFamilyName(familyName);
            }

            if (null != role) {
                userPrototype.setRole(role);
            }

            if (null != schoolOther) {
                userPrototype.setSchoolOther(schoolOther);
            }

            if (null != schoolURN) {
                userPrototype.setSchoolId(schoolURN);
            }

            List<RegisteredUserDTO> findUsers;

            // If a unique email address (without wildcards) provided, look up using this email immediately:
            if (null != email && !email.isEmpty() && !(email.contains("%") || email.contains("_"))) {
                try {
                    findUsers = Collections.singletonList(this.userManager.getUserDTOByEmail(email));
                } catch (NoUserException e) {
                    findUsers = Collections.emptyList();
                }
            } else {
                findUsers = this.userManager.findUsers(userPrototype);
            }

            // if postcode is set, filter found users
            if (null != postcode) {
                try {
                    Map<String, List<Long>> postCodeAndUserIds = Maps.newHashMap();
                    for (RegisteredUserDTO userDTO : findUsers) {
                        if (userDTO.getSchoolId() != null) {
                            School school = this.schoolReader.findSchoolById(userDTO.getSchoolId());
                            if (school != null) {
                                String schoolPostCode = school.getPostcode();
                                List<Long> ids;
                                if (postCodeAndUserIds.containsKey(schoolPostCode)) {
                                    ids = postCodeAndUserIds.get(schoolPostCode);
                                } else {
                                    ids = Lists.newArrayList();
                                }
                                ids.add(userDTO.getId());
                                postCodeAndUserIds.put(schoolPostCode, ids);
                            }
                        }
                    }

                    PostCodeRadius radius = PostCodeRadius.valueOf(postcodeRadius);

                    List<Long> userIdsWithinRadius = locationManager
                            .getUsersWithinPostCodeDistanceOf(postCodeAndUserIds, postcode, radius);

                    // Make sure the list returned is users who have schools in our postcode radius
                    List<RegisteredUserDTO> nearbyUsers = new ArrayList<>();
                    for (Long id : userIdsWithinRadius) {
                        RegisteredUserDTO user = this.userManager.getUserDTOById(id);
                        if (user != null) {
                            nearbyUsers.add(user);
                        }
                    }
                    findUsers = nearbyUsers;

                } catch (LocationServerException e) {
                    log.error("Location service unavailable. ", e);
                    return new SegueErrorResponse(Status.SERVICE_UNAVAILABLE,
                            "Unable to process request using 3rd party location provider").toResponse();
                } catch (UnableToIndexSchoolsException | SegueSearchException e) {
                    log.error("Unable to get school statistics", e);
                    return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR,
                            "Unable to process schools information").toResponse();
                } catch (JsonParseException | JsonMappingException e) {
                    log.error("Problem parsing school", e);
                    return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Unable to read school")
                            .toResponse();
                } catch (IOException e) {
                    log.error("Problem parsing school", e);
                    return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR,
                            "IOException while trying to communicate with the school service.").toResponse();
                } catch (NoUserException e) {
                    log.error("User cannot be found from user Id", e);
                }

            }

            // FIXME - this shouldn't really be in a segue class!
            if (subjectOfInterest != null && !subjectOfInterest.isEmpty()) {
                List<RegisteredUserDTO> subjectFilteredUsers = new ArrayList<>();
                Map<Long, List<UserPreference>> userPreferences = userPreferenceManager
                        .getUserPreferences(SUBJECT_INTEREST, findUsers);

                for (RegisteredUserDTO userToFilter : findUsers) {
                    if (userPreferences.containsKey(userToFilter.getId())) {
                        for (UserPreference pref : userPreferences.get(userToFilter.getId())) {
                            if (pref.getPreferenceName().equals(subjectOfInterest) && pref.getPreferenceValue()) {
                                subjectFilteredUsers.add(userToFilter);
                            }
                        }
                    }
                }
                findUsers = subjectFilteredUsers;
            }

            // Calculate the ETag
            EntityTag etag = new EntityTag(
                    findUsers.size() + findUsers.toString().hashCode() + userPrototype.toString().hashCode() + "");

            Response cachedResponse = generateCachedResponse(request, etag);
            if (cachedResponse != null) {
                return cachedResponse;
            }

            log.info(String.format("%s user (%s) did a search across all users based on user prototype {%s}",
                    currentUser.getRole(), currentUser.getEmail(), userPrototype));

            return Response.ok(findUsers).tag(etag)
                    .cacheControl(getCacheControl(NEVER_CACHE_WITHOUT_ETAG_CHECK, false)).build();
        } catch (SegueDatabaseException e) {
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR,
                    "Database error while looking up user information.").toResponse();
        }
    }

    /**
     * Get a user by id or email.
     * 
     * @param httpServletRequest
     *            - for checking permissions
     * @param userId
     *            - if searching by id
     * @return a userDTO or a segue error response
     */
    @GET
    @Path("/users/{user_id}")
    @Produces(MediaType.APPLICATION_JSON)
    @GZIP
    public Response findUsers(@Context final HttpServletRequest httpServletRequest,
            @PathParam("user_id") final Long userId) {

        RegisteredUserDTO currentUser;
        try {
            currentUser = userManager.getCurrentRegisteredUser(httpServletRequest);
            if (!isUserAnAdminOrEventManager(httpServletRequest)) {
                return new SegueErrorResponse(Status.FORBIDDEN,
                        "You must be logged in as an admin to access this function.").toResponse();
            }
        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        }

        try {
            log.info(String.format("%s user (%s) did a user id lookup based on user id {%s}", currentUser.getRole(),
                    currentUser.getEmail(), userId));

            return Response.ok(this.userManager.getUserDTOById(userId)).build();
        } catch (SegueDatabaseException e) {
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR,
                    "Database error while looking up user information.").toResponse();
        } catch (NoUserException e) {
            return new SegueErrorResponse(Status.NOT_FOUND,
                    "Unable to locate the user with the requested id: " + userId).toResponse();
        }
    }

    /**
     * Delete all user data for a particular user account.
     * 
     * @param httpServletRequest
     *            - for checking permissions
     * @param userId
     *            - the id of the user to delete.
     * @return a userDTO or a segue error response
     */
    @DELETE
    @Path("/users/{user_id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response deleteUserAccount(@Context final HttpServletRequest httpServletRequest,
            @PathParam("user_id") final Long userId) {
        try {
            if (!isUserAnAdmin(httpServletRequest)) {
                return new SegueErrorResponse(Status.FORBIDDEN,
                        "You must be logged in as an admin to access this function.").toResponse();
            }

            RegisteredUserDTO currentlyLoggedInUser = this.userManager.getCurrentRegisteredUser(httpServletRequest);
            if (currentlyLoggedInUser.getId().equals(userId)) {
                return new SegueErrorResponse(Status.BAD_REQUEST, "You are not allowed to delete yourself.")
                        .toResponse();
            }

            RegisteredUserDTO userToDelete = this.userManager.getUserDTOById(userId);

            this.userManager.deleteUserAccount(userToDelete);

            getLogManager().logEvent(currentlyLoggedInUser, httpServletRequest, DELETE_USER_ACCOUNT,
                    ImmutableMap.of(USER_ID_FKEY_FIELDNAME, userToDelete.getId()));

            log.info("Admin User: " + currentlyLoggedInUser.getEmail()
                    + " has just deleted the user account with id: " + userId);

            return Response.noContent().build();
        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        } catch (SegueDatabaseException e) {
            log.error("Unable to delete account", e);
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR,
                    "Database error while looking up user information.").toResponse();
        } catch (NoUserException e) {
            return new SegueErrorResponse(Status.NOT_FOUND,
                    "Unable to locate the user with the requested id: " + userId).toResponse();
        }
    }

    /**
     * Get users by school id.
     * 
     * @param request
     *            - to determine access.
     * @param schoolId
     *            - of the school of interest.
     * @return stats
     */
    @GET
    @Path("/users/schools/{school_id}")
    @Produces(MediaType.APPLICATION_JSON)
    @GZIP
    public Response getSchoolStatistics(@Context final HttpServletRequest request,
            @PathParam("school_id") final String schoolId) {
        try {
            if (!isUserStaff(request)) {
                return new SegueErrorResponse(Status.FORBIDDEN,
                        "You must be an admin user to access this endpoint.").toResponse();
            }

            School school = schoolReader.findSchoolById(schoolId);

            Map<String, Object> result = ImmutableMap.of("school", school, "users",
                    statsManager.getUsersBySchoolId(schoolId));

            return Response.ok(result).build();
        } catch (UnableToIndexSchoolsException e) {
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR,
                    "Unable To Index SchoolIndexer Exception in admin facade", e).toResponse();
        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        } catch (ResourceNotFoundException e) {
            return new SegueErrorResponse(Status.NOT_FOUND, "We cannot locate the school requested").toResponse();
        } catch (SegueDatabaseException | SegueSearchException e) {
            log.error("Error while trying to list users belonging to a school.", e);
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Database error").toResponse();
        } catch (NumberFormatException e) {
            return new SegueErrorResponse(Status.BAD_REQUEST, "The school id provided is invalid.").toResponse();
        } catch (JsonParseException | JsonMappingException e) {
            log.error("Problem parsing school", e);
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Unable to read school").toResponse();
        } catch (IOException e) {
            log.error("Problem parsing school", e);
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR,
                    "IOException while trying to communicate with the school service.").toResponse();
        }
    }

    /**
     * Service method to fetch the event data for a specified user.
     *
     * @param request
     *            - request information used authentication
     * @param requestForCaching
     *            - request information used for caching.
     * @param httpServletRequest
     *            - the request which may contain session information.
     * @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.
     * @throws BadRequestException
     *            - because the request is missing various essential parameters
     * @throws ForbiddenException
     *            - because the user is not an admin
     * @throws NoUserLoggedInException
     *            - because the user is not logged in
     * @throws SegueDatabaseException
     *            - because there has been some problem with database access
     */
    private Map<String, Map<LocalDate, Long>> fetchEventDataForAllUsers(@Context final Request request,
            @Context final HttpServletRequest httpServletRequest, @Context final Request requestForCaching,
            @QueryParam("from_date") final Long fromDate, @QueryParam("to_date") final Long toDate,
            @QueryParam("events") final String events, @QueryParam("bin_data") final Boolean bin)
            throws BadRequestException, ForbiddenException, NoUserLoggedInException, SegueDatabaseException {

        final boolean binData = null != bin && bin;

        if (null == events || events.isEmpty()) {
            throw new BadRequestException("You must specify the events you are interested in.");
        }

        if (null == fromDate || null == toDate) {
            throw new BadRequestException("You must specify the from_date and to_date you are interested in.");
        }

        if (!isUserStaff(httpServletRequest)) {
            throw new ForbiddenException("You must be logged in as an admin to access this function.");
        }

        return this.statsManager.getEventLogsByDate(Lists.newArrayList(events.split(",")), new Date(fromDate),
                new Date(toDate), binData);
    }

    /**
     * Get the event data for a specified user.
     * 
     * @param request
     *            - request information used authentication
     * @param requestForCaching
     *            - request information used for caching.
     * @param httpServletRequest
     *            - the request which may contain session information.
     * @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/event_data/over_time")
    @Produces(MediaType.APPLICATION_JSON)
    @GZIP
    public Response getEventDataForAllUsers(@Context final Request request,
            @Context final HttpServletRequest httpServletRequest, @Context final Request requestForCaching,
            @QueryParam("from_date") final Long fromDate, @QueryParam("to_date") final Long toDate,
            @QueryParam("events") final String events, @QueryParam("bin_data") final Boolean bin) {

        Map<String, Map<LocalDate, Long>> eventLogsByDate;
        try {
            eventLogsByDate = fetchEventDataForAllUsers(request, httpServletRequest, requestForCaching, fromDate,
                    toDate, events, bin);
        } catch (BadRequestException e) {
            return new SegueErrorResponse(Status.BAD_REQUEST, e.getMessage()).toResponse();
        } catch (ForbiddenException e) {
            return new SegueErrorResponse(Status.FORBIDDEN, e.getMessage()).toResponse();
        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        } catch (SegueDatabaseException e) {
            log.error("Database error while getting event details for a user.", e);
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Unable to complete the request.")
                    .toResponse();
        }

        EntityTag etag = new EntityTag(eventLogsByDate.toString().hashCode() + "");

        Response cachedResponse = generateCachedResponse(requestForCaching, etag);
        if (cachedResponse != null) {
            return cachedResponse;
        }

        return Response.ok(eventLogsByDate).tag(etag)
                .cacheControl(getCacheControl(NUMBER_SECONDS_IN_FIVE_MINUTES, false)).build();
    }

    /**
     * Get the event data for a specified user, in CSV format.
     *
     * @param request
     *            - request information used authentication
     * @param requestForCaching
     *            - request information used for caching.
     * @param httpServletRequest
     *            - the request which may contain session information.
     * @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/event_data/over_time/download")
    @Produces("text/csv")
    @GZIP
    public Response getEventDataForAllUsersDownloadCSV(@Context final Request request,
            @Context final HttpServletRequest httpServletRequest, @Context final Request requestForCaching,
            @QueryParam("from_date") final Long fromDate, @QueryParam("to_date") final Long toDate,
            @QueryParam("events") final String events, @QueryParam("bin_data") final Boolean bin) {

        try {
            Map<String, Map<LocalDate, Long>> eventLogsByDate;
            eventLogsByDate = fetchEventDataForAllUsers(request, httpServletRequest, requestForCaching, fromDate,
                    toDate, events, bin);

            StringWriter stringWriter = new StringWriter();
            CSVWriter csvWriter = new CSVWriter(stringWriter);
            List<String[]> rows = Lists.newArrayList();
            rows.add(new String[] { "event_type", "timestamp", "value" });

            for (Map.Entry<String, Map<LocalDate, Long>> eventType : eventLogsByDate.entrySet()) {
                String eventTypeKey = eventType.getKey();
                for (Map.Entry<LocalDate, Long> record : eventType.getValue().entrySet()) {
                    rows.add(new String[] { eventTypeKey, record.getKey().toString(),
                            record.getValue().toString() });
                }
            }
            csvWriter.writeAll(rows);
            csvWriter.close();

            EntityTag etag = new EntityTag(eventLogsByDate.toString().hashCode() + "");
            Response cachedResponse = generateCachedResponse(requestForCaching, etag);
            if (cachedResponse != null) {
                return cachedResponse;
            }

            return Response.ok(stringWriter.toString()).tag(etag)
                    .header("Content-Disposition", "attachment; filename=admin_stats.csv")
                    .cacheControl(getCacheControl(NUMBER_SECONDS_IN_FIVE_MINUTES, false)).build();
        } catch (BadRequestException e) {
            return new SegueErrorResponse(Status.BAD_REQUEST, e.getMessage()).toResponse();
        } catch (ForbiddenException e) {
            return new SegueErrorResponse(Status.FORBIDDEN, e.getMessage()).toResponse();
        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        } catch (SegueDatabaseException e) {
            log.error("Database error while getting event details for a user.", e);
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Unable to complete the request.")
                    .toResponse();
        } catch (IOException e) {
            log.error("IO error while creating the CSV file.", e);
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Error while creating the CSV file")
                    .toResponse();
        }
    }

    /**
     * Get the ip address location information.
     * 
     * @param request
     *            - request information used for caching.
     * @param httpServletRequest
     *            - the request which may contain session information.
     * @param ipaddress
     *            - ip address to geocode.
     * @return Returns a location from an ip address
     */
    @GET
    @Path("/geocode_ip")
    @Produces(MediaType.APPLICATION_JSON)
    @GZIP
    public Response findIP(@Context final Request request, @Context final HttpServletRequest httpServletRequest,
            @QueryParam("ip") final String ipaddress) {

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

        try {
            if (!isUserStaff(httpServletRequest)) {
                return new SegueErrorResponse(Status.FORBIDDEN,
                        "You must be logged in as staff to access this function.").toResponse();
            }

            return Response.ok(locationManager.resolveAllLocationInformation(ipaddress)).build();
        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        } catch (IOException e) {
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR,
                    "Unable to contact server to resolve location information", e).toResponse();
        } catch (LocationServerException e) {
            return new SegueErrorResponse(Status.BAD_REQUEST, "Problem resolving ip address", e).toResponse();
        }
    }

    /**
     * Get current perf log for analytics purposes.
     * 
     * @param request
     *            - request information used for caching.
     * @param httpServletRequest
     *            - the request which may contain session information.
     * @return Returns a location from an ip address
     */
    @GET
    @Path("/view_perf_log")
    @Produces(MediaType.APPLICATION_JSON)
    @GZIP
    public Response viewPerfLog(@Context final Request request,
            @Context final HttpServletRequest httpServletRequest) {
        try (BufferedReader bufferedReader = new BufferedReader(new FileReader(
                System.getProperty("catalina.base") + File.separator + "logs" + File.separator + "perf.log"))) {
            if (!isUserAnAdmin(httpServletRequest)) {
                return new SegueErrorResponse(Status.FORBIDDEN,
                        "You must be logged in as staff to access this function.").toResponse();
            }

            StringBuilder output = new StringBuilder();
            String line;
            while ((line = bufferedReader.readLine()) != null) {

                if (line.contains("callback")) {
                    // strip out query params that are provided on callback urls for security.
                    output.append(line.substring(0, line.indexOf("?")));
                } else {
                    output.append(line);
                }
                output.append("\n");
            }
            log.info(String.format("User (%s) has accessed the perf logs.",
                    userManager.getCurrentRegisteredUser(httpServletRequest).getEmail()));
            return Response.ok(output.toString()).build();
        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        } catch (IOException e) {
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Unable to read the log file requested", e)
                    .toResponse();
        }
    }

    /**
     * Get current questionMetaData for analytics purposes.
     * 
     * @param request
     *            - request information used for caching.
     * @param httpServletRequest
     *            - the request which may contain session information.
     * @return Returns a location from an ip address
     */
    @GET
    @Path("/download_meta_data")
    @Produces(MediaType.APPLICATION_JSON)
    @GZIP
    public Response getMetaData(@Context final Request request,
            @Context final HttpServletRequest httpServletRequest) {
        try {
            if (!isUserStaff(httpServletRequest)) {
                return new SegueErrorResponse(Status.FORBIDDEN,
                        "You must be logged in as staff to access this function.").toResponse();
            }

            ResultsWrapper<ContentDTO> allByType = this.contentManager.getAllByTypeRegEx(this.contentIndex, ".*", 0,
                    -1);

            List<ContentSummaryDTO> results = Lists.newArrayList();
            for (ContentDTO c : allByType.getResults()) {
                results.add(this.contentManager.extractContentSummary(c));
            }

            ResultsWrapper<ContentSummaryDTO> toReturn = new ResultsWrapper<>(results, allByType.getTotalResults());
            return Response.ok(toReturn).build();
        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        } catch (ContentManagerException e) {
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Unable to read the log file requested", e)
                    .toResponse();
        }
    }

    /**
     * Is the current user an admin.
     * 
     * @param request
     *            - with session information
     * @return true if user is logged in as an admin, false otherwise.
     * @throws NoUserLoggedInException
     *             - if we are unable to tell because they are not logged in.
     */
    private boolean isUserAnAdmin(final HttpServletRequest request) throws NoUserLoggedInException {
        return isUserAnAdmin(userManager, request);
    }

    /**
     * Is the current user an admin.
     * 
     * @param request
     *            - with session information
     * @return true if user is logged in as an admin, false otherwise.
     * @throws NoUserLoggedInException
     *             - if we are unable to tell because they are not logged in.
     */
    private boolean isUserAnAdminOrEventManager(final HttpServletRequest request) throws NoUserLoggedInException {
        return isUserAnAdminOrEventManager(userManager, request);
    }

    /**
     * Is the current user in a staff role.
     * 
     * @param request
     *            - with session information
     * @return true if user is logged in as an admin, false otherwise.
     * @throws NoUserLoggedInException
     *             - if we are unable to tell because they are not logged in.
     */
    private boolean isUserStaff(final HttpServletRequest request) throws NoUserLoggedInException {
        return isUserStaff(userManager, request);
    }

    /**
     * This method will allow the live version served by the site to be changed.
     *
     * @param request
     *            - to help determine access rights.
     * @param version
     *            - version to use as updated version of content store.
     * @return Success shown by returning the new liveSHA or failed message "Invalid version selected".
     */
    @POST
    @Path("/live_version/{version}")
    @Produces(MediaType.APPLICATION_JSON)
    public synchronized Response changeLiveVersion(@Context final HttpServletRequest request,
            @PathParam("version") final String version) {

        try {
            if (isUserAnAdmin(request)) {

                String oldLiveVersion = contentManager.getCurrentContentSHA();

                HttpClient httpClient = new DefaultHttpClient();

                HttpPost httpPost = new HttpPost("http://" + getProperties().getProperty("ETL_HOSTNAME") + ":"
                        + getProperties().getProperty("ETL_PORT") + "/isaac-api/api/etl/set_version_alias/"
                        + this.contentIndex + "/" + version);

                httpPost.addHeader("Content-Type", "application/json");

                HttpResponse httpResponse = httpClient.execute(httpPost);

                HttpEntity e = httpResponse.getEntity();

                if (httpResponse.getStatusLine().getStatusCode() == 200) {
                    log.info(userManager.getCurrentRegisteredUser(request).getEmail()
                            + " changed live version from " + oldLiveVersion + " to " + version + ".");
                    return Response.ok().build();
                } else {
                    SegueErrorResponse r = new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR,
                            IOUtils.toString(e.getContent()));
                    r.setBypassGenericSiteErrorPage(true);
                    return r.toResponse();
                }

            } else {
                return new SegueErrorResponse(Status.FORBIDDEN,
                        "You must be logged in as an admin to access this function.").toResponse();
            }
        } catch (NoUserLoggedInException e) {
            return SegueErrorResponse.getNotLoggedInResponse();
        } catch (Exception e) {
            log.error("Exception during version change.", e);
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, "Error during verison change.", e)
                    .toResponse();
        }
    }

    @POST
    @Path("/new_version_alert")
    @Produces(MediaType.APPLICATION_JSON)
    public Response versionChangeNotification(GithubPushEventPayload payload) {

        // TODO: Verify webhook secret.
        try {
            // We are only interested in the master branch
            if (payload.getRef().equals("refs/heads/master")) {
                String newVersion = payload.getAfter();

                HttpPost httpPost = new HttpPost("http://" + getProperties().getProperty("ETL_HOSTNAME") + ":"
                        + getProperties().getProperty("ETL_PORT") + "/isaac-api/api/etl/new_version_alert/"
                        + newVersion);

                HttpResponse httpResponse;
                httpResponse = new DefaultHttpClient().execute(httpPost);
                HttpEntity e = httpResponse.getEntity();

                if (httpResponse.getStatusLine().getStatusCode() == 200) {
                    return Response.ok().build();
                } else {
                    SegueErrorResponse r = new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR,
                            IOUtils.toString(e.getContent()));
                    r.setBypassGenericSiteErrorPage(true);
                    return r.toResponse();
                }
            }
        } catch (IOException e) {
            return new SegueErrorResponse(Status.INTERNAL_SERVER_ERROR, e.getMessage()).toResponse();
        }
        return Response.ok().build();
    }

}