nl.surfnet.coin.api.ApiController.java Source code

Java tutorial

Introduction

Here is the source code for nl.surfnet.coin.api.ApiController.java

Source

/*
 * Copyright 2012 SURFnet bv, The Netherlands
 *
 * 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 nl.surfnet.coin.api;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;

import javax.annotation.Resource;

import nl.surfnet.coin.api.GroupProviderConfiguration.Service;
import nl.surfnet.coin.api.client.domain.AbstractEntry;
import nl.surfnet.coin.api.client.domain.Group20;
import nl.surfnet.coin.api.client.domain.Group20Entry;
import nl.surfnet.coin.api.client.domain.GroupEntry;
import nl.surfnet.coin.api.client.domain.GroupMembersEntry;
import nl.surfnet.coin.api.client.domain.Person;
import nl.surfnet.coin.api.client.domain.PersonEntry;
import nl.surfnet.coin.api.oauth.ClientMetaData;
import nl.surfnet.coin.api.service.GroupService;
import nl.surfnet.coin.api.service.PersonService;
import nl.surfnet.coin.eb.EngineBlock;
import nl.surfnet.coin.ldap.LdapClient;
import nl.surfnet.coin.shared.domain.ErrorMail;
import nl.surfnet.coin.shared.log.ApiCallLog;
import nl.surfnet.coin.shared.log.ApiCallLogContextListener;
import nl.surfnet.coin.shared.log.ApiCallLogService;
import nl.surfnet.coin.shared.service.ErrorMessageMailer;
import nl.surfnet.coin.teams.domain.GroupProvider;
import nl.surfnet.coin.teams.domain.GroupProviderType;
import nl.surfnet.coin.teams.domain.TeamExternalGroup;
import nl.surfnet.coin.teams.service.TeamExternalGroupDao;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Transformer;
import org.apache.commons.lang.StringUtils;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

/**
 * 
 * We wildcard all {@link RequestMapping} with ':.+' (see
 * http://stackoverflow.com
 * /questions/3526523/spring-mvc-pathvariable-getting-truncated)
 * 
 */
public class ApiController extends AbstractApiController {

    private static Logger LOG = LoggerFactory.getLogger(ApiController.class);

    public static final String GROUP_ID_SELF = "@self";
    public static final String PERSON_ID_SELF = "@me";

    protected PersonService personService;

    protected GroupService groupService;

    protected EngineBlock engineBlock;

    protected GroupProviderConfiguration groupProviderConfiguration;

    @Autowired
    protected TeamExternalGroupDao teamExternalGroupDao;

    @Resource(name = "errorMessageMailer")
    private ErrorMessageMailer errorMessageMailer;

    @Autowired
    private ApiCallLogService logService;

    @RequestMapping(method = RequestMethod.GET, value = "/people/{userId:.+}")
    @ResponseBody
    public PersonEntry getPerson(@PathVariable("userId") String userId) {
        String onBehalfOf = getOnBehalfOf();
        LOG.info("Got getPerson-request, for userId '{}' on behalf of '{}'", userId, onBehalfOf);
        if (PERSON_ID_SELF.equals(userId)) {
            userId = onBehalfOf;
        }
        List<GroupProvider> allGroupProviders = getAllAllowedGroupProviders(Service.People);
        GroupProvider grouper = getGrouperProvider(allGroupProviders);
        String spEntityId = getClientMetaData().getAppEntityId();
        // sensible default
        PersonEntry person = new PersonEntry();
        if (!groupProviderConfiguration.isCallAllowed(Service.People, spEntityId, grouper)) {
            sendAclMissingMail(grouper, spEntityId, userId, Service.People);
        } else {
            person = personService.getPerson(userId, onBehalfOf, spEntityId);
        }
        logApiCall(onBehalfOf);
        setResultOptions(person, 0, 0, null);
        return person;
    }

    @RequestMapping(method = RequestMethod.GET, value = "/people/{userId:.+}/{groupId:.+}")
    @ResponseBody
    public Object getGroupMembers(@PathVariable("userId") String userId, @PathVariable("groupId") String groupId,
            @RequestParam(value = "count", required = false) Integer count,
            @RequestParam(value = "startIndex", required = false) Integer startIndex,
            @RequestParam(value = "sortBy", required = false) String sortBy) {
        String onBehalfOf = getOnBehalfOf();
        validateRequest(userId, onBehalfOf);
        if (PERSON_ID_SELF.equals(userId)) {
            userId = onBehalfOf;
        }
        if (GROUP_ID_SELF.equals(groupId)) {
            // Backwards compatibility with os.surfconext.
            return getPerson(userId);
        }
        String spEntityId = getClientMetaData().getAppEntityId();
        if (!userId.startsWith(LdapClient.URN_IDENTIFIER)) {
            // persistent identifier, need urn to query
            PersonEntry person = personService.getPerson(userId, onBehalfOf, spEntityId);
            userId = person.getEntry().getId();
        }
        if (onBehalfOf == null) {
            onBehalfOf = userId;
        }
        LOG.info("Got getGroupMembers-request, for userId '{}', groupId '{}', on behalf of '{}'",
                new Object[] { userId, groupId, onBehalfOf });
        List<GroupProvider> allGroupProviders = getAllAllowedGroupProviders(Service.People);
        // sensible default
        GroupMembersEntry groupMembers = new GroupMembersEntry(new ArrayList<Person>());
        GroupProvider grouper = getGrouperProvider(allGroupProviders);
        boolean grouperAllowed = groupProviderConfiguration.isCallAllowed(Service.People, spEntityId, grouper);
        boolean internalGroup = groupProviderConfiguration.isInternalGroup(groupId);
        if (!grouperAllowed) {
            sendAclMissingMail(grouper, spEntityId, userId, Service.People);
        }
        if (grouperAllowed && internalGroup) {
            // need to cut off the urn part in order for Grouper
            String grouperGroupId = groupProviderConfiguration.cutOffUrnPartForGrouper(allGroupProviders, groupId);
            //we don't want the original count / startindex as we will sublist after
            groupMembers = personService.getGroupMembers(grouperGroupId, onBehalfOf, spEntityId, null, null,
                    sortBy);
        }
        if (!internalGroup) {
            // external group. see which groupProvider can handle this call
            for (GroupProvider groupProvider : allGroupProviders) {
                /*
                 * Do we need to make calls to this external group provider?
                 */
                if (groupProvider.isExternalGroupProvider() && groupProvider.isMeantForUser(onBehalfOf)) {
                    GroupMembersEntry externalGroupMembers = groupProviderConfiguration
                            .getGroupMembersEntry(groupProvider, onBehalfOf, groupId, Integer.MAX_VALUE, 0);
                    if (externalGroupMembers != null) {
                        List<Person> entry = externalGroupMembers.getEntry();
                        if (entry != null) {
                            // Apply ARP
                            entry = personService.enforceArp(spEntityId, entry);
                            groupMembers.getEntry().addAll(entry);
                        }
                    }
                }
            }
            /*
             * Is this an external group that is linked to a SURFteams group?
             */
            Group20Entry grouperTeams = getGrouperTeamsLinkedToExternalGroup(userId, groupId);
            if (grouperTeams != null && grouperTeams.getEntry() != null && !grouperTeams.getEntry().isEmpty()) {
                for (Group20 group20 : grouperTeams.getEntry()) {
                    GroupMembersEntry members = personService.getGroupMembers(group20.getId(), userId, spEntityId,
                            null, null, null);
                    groupMembers.getEntry().addAll(members.getEntry());
                }
            }
        }
        logApiCall(onBehalfOf);
        setResultOptions(groupMembers, count, startIndex, sortBy);
        return groupMembers;
    }

    @RequestMapping(method = RequestMethod.GET, value = "/groups/{userId:.+}")
    @ResponseBody
    public Group20Entry getGroups(@PathVariable("userId") String userId,
            @RequestParam(value = "count", required = false) Integer count,
            @RequestParam(value = "startIndex", required = false) Integer startIndex,
            @RequestParam(value = "sortBy", required = false) String sortBy) {
        invariant();
        String onBehalfOf = getOnBehalfOf();
        String spEntityId = getClientMetaData().getAppEntityId();
        if (PERSON_ID_SELF.equals(userId)) {
            userId = onBehalfOf;
        } else if (!userId.startsWith(LdapClient.URN_IDENTIFIER)) {
            // persistent identifier, need urn to query
            PersonEntry person = personService.getPerson(userId, onBehalfOf, spEntityId);
            userId = person.getEntry().getId();
        }
        LOG.info("Got getGroups-request, for userId '{}',  on behalf of '{}'", new Object[] { userId, onBehalfOf });

        List<GroupProvider> allGroupProviders = getAllAllowedGroupProviders(Service.Group);
        // sensible default
        Group20Entry group20Entry = new Group20Entry(new ArrayList<Group20>());

        GroupProvider grouper = getGrouperProvider(allGroupProviders);
        boolean grouperAllowed = groupProviderConfiguration.isCallAllowed(Service.Group, spEntityId, grouper);
        if (!grouperAllowed) {
            sendAclMissingMail(grouper, spEntityId, userId, Service.People);
        } else {
            //we don't want the original count / startindex as we will sublist after
            group20Entry = groupService.getGroups20(userId, onBehalfOf, null, null, sortBy);
            if (group20Entry != null && !group20Entry.getEntry().isEmpty()) {
                group20Entry = groupProviderConfiguration.addUrnPartForGrouper(allGroupProviders, group20Entry);
            }
        }

        List<Group20> listOfAllExternalGroups = new ArrayList<Group20>();
        // Now see which external groupProvider can also handle this call
        for (GroupProvider groupProvider : allGroupProviders) {
            /*
             * Do we need to make calls this external group provider?
             */
            if (groupProvider.isExternalGroupProvider() && groupProvider.isMeantForUser(userId)) {
                Group20Entry externalGroups = groupProviderConfiguration.getGroup20Entry(groupProvider, userId,
                        Integer.MAX_VALUE, 0);
                if (externalGroups != null) {
                    List<Group20> groups = externalGroups.getEntry();
                    if (groups != null) {
                        listOfAllExternalGroups.addAll(groups);
                    }
                }
            }
        }
        if (!listOfAllExternalGroups.isEmpty()) {
            group20Entry.getEntry().addAll(listOfAllExternalGroups);
            group20Entry = addLinkedSurfTeamGroupsForExternalGroups(userId, group20Entry, listOfAllExternalGroups,
                    allGroupProviders);
        }

        logApiCall(onBehalfOf);
        setResultOptions(group20Entry, count, startIndex, sortBy);
        return group20Entry;
    }

    @RequestMapping(method = RequestMethod.GET, value = "/osgroups/{userId:.+}")
    @ResponseBody
    public GroupEntry getOsGroups(@PathVariable("userId") String userId,
            @RequestParam(value = "count", required = false) Integer count,
            @RequestParam(value = "startIndex", required = false) Integer startIndex,
            @RequestParam(value = "sortBy", required = false) String sortBy) {
        return new GroupEntry(getGroups(userId, count, startIndex, sortBy));

    }

    /*
     * Get all SURFTeams groups for those external groups that linked to one or
     * more SURFTeams. Add those teams to the final list to return.
     */
    @SuppressWarnings("unchecked")
    private Group20Entry getGrouperTeamsLinkedToExternalGroup(String userId, String externalGroupId) {
        // sensible default
        Group20Entry groups20Entry = new Group20Entry(new ArrayList<Group20>());
        List<TeamExternalGroup> linkedExternalGroups = teamExternalGroupDao
                .getByExternalGroupIdentifier(externalGroupId);
        if (!CollectionUtils.isEmpty(linkedExternalGroups)) {
            List<String> grouperTeamIds = new ArrayList<String>();
            for (TeamExternalGroup teamExternalGroup : linkedExternalGroups) {
                grouperTeamIds.add(teamExternalGroup.getGrouperTeamId());
            }
            groups20Entry = groupService.getGroups20ByIds(userId,
                    grouperTeamIds.toArray(new String[grouperTeamIds.size()]), 0, 0);
        }
        return groups20Entry;
    }

    private void validateRequest(final String userId, final String onBehalfOf) {
        if (StringUtils.isEmpty(onBehalfOf) && PERSON_ID_SELF.equals(userId)) {
            throw new IllegalArgumentException(
                    "Argument " + PERSON_ID_SELF + " may not be specified in this context");
        }
    }

    /*
     * Get all SURFTeams groups for those external groups that linked to one or
     * more SURFTeams. Add those teams to the final list to return.
     */
    @SuppressWarnings("unchecked")
    private Group20Entry addLinkedSurfTeamGroupsForExternalGroups(String userId, Group20Entry group20Entry,
            List<Group20> listOfAllExternalGroups, List<GroupProvider> allGroupProviders) {
        List<String> grouperTeamIds = new ArrayList<String>();
        Collection<String> identifiers = CollectionUtils.collect(listOfAllExternalGroups, new Transformer() {
            @Override
            public Object transform(Object input) {
                return ((Group20) input).getId();
            }
        });
        List<TeamExternalGroup> externalGroupIdentifiers = teamExternalGroupDao
                .getByExternalGroupIdentifiers(identifiers);
        for (TeamExternalGroup teamExternalGroup : externalGroupIdentifiers) {
            grouperTeamIds.add(teamExternalGroup.getGrouperTeamId());
        }
        if (!grouperTeamIds.isEmpty()) {
            Group20Entry linkedTeams = groupService.getGroups20ByIds(userId,
                    grouperTeamIds.toArray(new String[grouperTeamIds.size()]), 0, 0);
            linkedTeams = groupProviderConfiguration.addUrnPartForGrouper(allGroupProviders, linkedTeams);

            group20Entry.getEntry().addAll(linkedTeams.getEntry());
            // remove duplicates: convert to set and back.
            group20Entry.setEntry(new ArrayList<Group20>(new HashSet<Group20>(group20Entry.getEntry())));
        }
        return group20Entry;
    }

    @RequestMapping(method = RequestMethod.GET, value = "/groups/{userId:.+}/{groupId}")
    @ResponseBody
    public Group20Entry getGroup(@PathVariable("userId") String userId, @PathVariable("groupId") String groupId) {
        invariant();
        String onBehalfOf = getOnBehalfOf();
        String spEntityId = getClientMetaData().getAppEntityId();
        if (PERSON_ID_SELF.equals(userId)) {
            userId = onBehalfOf;
        } else if (!userId.startsWith(LdapClient.URN_IDENTIFIER)) {
            // persistent identifier, need urn to query
            PersonEntry person = personService.getPerson(userId, userId, spEntityId);
            userId = person.getEntry().getId();
        }
        if (onBehalfOf == null) {
            onBehalfOf = userId;
        }
        List<GroupProvider> allGroupProviders = getAllAllowedGroupProviders(Service.Group);
        // sensible default
        Group20Entry group20Entry = new Group20Entry(new ArrayList<Group20>());

        GroupProvider grouper = getGrouperProvider(allGroupProviders);
        boolean grouperAllowed = groupProviderConfiguration.isCallAllowed(Service.Group, spEntityId, grouper);
        if (!grouperAllowed) {
            sendAclMissingMail(grouper, spEntityId, userId, Service.Group);
        }
        List<Group20> externalGroups = new ArrayList<Group20>();
        /*
         * Is the call to Grouper necessary (e.g. is this an internal group)?
         */
        if (grouperAllowed && groupProviderConfiguration.isInternalGroup(groupId)) {
            // need to cut off the urn part in order for Grouper
            String grouperGroupId = groupProviderConfiguration.cutOffUrnPartForGrouper(allGroupProviders, groupId);
            group20Entry = groupService.getGroup20(userId, grouperGroupId, onBehalfOf);
            if (group20Entry != null && group20Entry.getEntry() != null && group20Entry.getEntry().isEmpty()) {
                group20Entry = groupProviderConfiguration.addUrnPartForGrouper(allGroupProviders, group20Entry);
            }
        } else {
            // external group. see which groupProvider can handle this call
            for (GroupProvider groupProvider : allGroupProviders) {
                /*
                 * Do we need to make calls to this external group provider?
                 */
                if (groupProvider.isExternalGroupProvider() && groupProvider.isMeantForUser(onBehalfOf)) {
                    Group20 group = groupProviderConfiguration.getGroup20(groupProvider, userId, groupId);
                    if (group != null) {
                        externalGroups.add(group);
                    }
                }
            }
            if (!CollectionUtils.isEmpty(externalGroups)) {
                group20Entry.getEntry().addAll(externalGroups);
                group20Entry = addLinkedSurfTeamGroupsForExternalGroups(userId, group20Entry, externalGroups,
                        allGroupProviders);

            }

        }
        logApiCall(onBehalfOf);
        setResultOptions(group20Entry, 0, 0, null);
        return group20Entry;
    }

    /**
     * Handler for RuntimeExceptions. It makes API return a model containing the original exception's message.
     * @param e the exception
     * @return the response body
     */
    @ResponseStatus(value = HttpStatus.NOT_FOUND)
    @ResponseBody
    @ExceptionHandler(RuntimeException.class)
    public Object handleRuntimeException(RuntimeException e) {
        LOG.error("Handling generic runtime exception, will respond with HTTP Not Found.", e);
        return new Group20Entry(new ArrayList<Group20>());
    }

    /**
     * Handler for IllegalArgument Exception.
     * @param e the exception
     * @return the response body
     */
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ResponseBody
    @ExceptionHandler(IllegalArgumentException.class)
    public Object handleIllegalArgumentException(IllegalArgumentException e) {
        LOG.info("Illegal Argument encountered in client request, we are responding with HTTP Bad Request");
        return new Group20Entry(new ArrayList<Group20>());
    }

    /*
     * Set the metadata for the result
     */
    protected void setResultOptions(AbstractEntry entry, Integer count, Integer startIndex, String sortBy) {
        entry.setFiltered(false);
        entry.setSorted(sortBy != null);
        if (sortBy != null) {
            entry.sortEntryCollection(sortBy);
        }
        entry.setUpdatedSince(false);
        int entrySize = entry.getEntrySize();
        entry.setTotalResults(entrySize);
        entry.setStartIndex((startIndex != null && startIndex != 0) ? startIndex : 0);
        /*
         * Bugfix: https://jira.surfconext.nl/jira/browse/BACKLOG-739
         * 
         * We might get more results then we want, because the result is plus the external group providers
         */
        count = (count != null && count != 0) ? count : 0;

        startIndex = entry.getStartIndex();
        if (startIndex > 0 || count > 0) {
            // need sublist
            List collection = entry.getEntryCollection();
            startIndex = startIndex > entrySize ? entrySize : startIndex;
            count = ((startIndex + count) < entrySize && count != 0) ? (startIndex + count) : entrySize;
            List subList = collection.subList(startIndex, count);
            entry.setEntryCollection(subList);
        }
        entry.setItemsPerPage(entry.getEntrySize());
    }

    protected void invariant() {
    }

    /*
     * Send a mail
     */
    protected void sendAclMissingMail(GroupProvider groupProvider, String spEntityId, String identifier,
            Service service) {
        String shortMessage = "Unauthorized attempt to api.surfconext";
        String formattedMessage = String.format(
                "Service Provider '%s' attempts to call '%s' on groupProvider '%s' for identifer '%s'", spEntityId,
                service, groupProvider, identifier);
        ErrorMail errorMail = new ErrorMail(shortMessage, formattedMessage, formattedMessage, getHost(), "API");
        errorMail.setLocation(this.getClass().getName() + "#get" + service);
        errorMessageMailer.sendErrorMail(errorMail);
    }

    protected List<GroupProvider> getAllAllowedGroupProviders(Service service) {
        List<GroupProvider> allGroupProviders = groupProviderConfiguration.getAllGroupProviders();
        String spEntityId = getClientMetaData().getAppEntityId();
        return groupProviderConfiguration.getAllowedGroupProviders(service, spEntityId, allGroupProviders);

    }

    private String getHost() {
        try {
            return InetAddress.getLocalHost().toString();
        } catch (UnknownHostException e) {
            return "UNKNOWN";
        }
    }

    /*
     * Save the fact that a request is made
     */
    protected void logApiCall(String onBehalfOf) {
        ApiCallLog log = ApiCallLogContextListener.getApiCallLog();
        ClientMetaData clientMetaData = getClientMetaData();
        log.setSpEntityId(clientMetaData.getAppEntityId());
        log.setConsumerKey(clientMetaData.getConsumerKey());
        log.setUserId(onBehalfOf);
        addApiCallLogInfo(log);
        logService.saveApiCallLog(log);
    }

    /**
     * Hook for subclasses to change the log record
     * 
     * @param log the log record
     */
    protected void addApiCallLogInfo(ApiCallLog log) {

    }

    private GroupProvider getGrouperProvider(List<GroupProvider> allGroupProviders) {
        for (GroupProvider groupProvider : allGroupProviders) {
            if (groupProvider.getGroupProviderType().equals(GroupProviderType.GROUPER)) {
                return groupProvider;
            }
        }
        return null;
    }

}