com.streamreduce.rest.resource.api.ConnectionResource.java Source code

Java tutorial

Introduction

Here is the source code for com.streamreduce.rest.resource.api.ConnectionResource.java

Source

/*
 * Copyright 2012 Nodeable Inc
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.streamreduce.rest.resource.api;

import com.google.common.collect.ImmutableMap;
import com.streamreduce.ConnectionNotFoundException;
import com.streamreduce.ConnectionTypeConstants;
import com.streamreduce.ProviderIdConstants;
import com.streamreduce.connections.ConnectionProvider;
import com.streamreduce.connections.ConnectionProviderFactory;
import com.streamreduce.core.model.*;
import com.streamreduce.core.service.ConnectionService;
import com.streamreduce.core.service.InventoryService;
import com.streamreduce.core.service.exception.ConnectionExistsException;
import com.streamreduce.core.service.exception.InvalidCredentialsException;
import com.streamreduce.rest.dto.response.*;
import com.streamreduce.rest.resource.ErrorMessages;
import com.streamreduce.util.AbstractProjectHostingClient;
import com.streamreduce.util.ConnectionUtils;
import com.streamreduce.util.JiraClient;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.annotations.ApiParam;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.xml.soap.SOAPException;
import java.io.IOException;
import java.net.UnknownHostException;
import java.util.*;

/**
 * A Connection includes: a globally unique identifier, a provider and type identifying the provider of an external
 * integration and the interaction model with that provider, an authentication type that determines how Nodeable will
 * authenticate to a connection on a user's behalf, an account unique alias, an optional description, hashtags associated
 * to the connection, and the visibility scope of all messages created from that account.
 */
@Component
@Path("api/connections")
@Api(value = "/api/connections", description = "A Connection is an abstraction that provides a top level "
        + "description of how Nodeable will connect to external providers on behalf of users.")
public class ConnectionResource extends AbstractOwnableResource {

    @Autowired
    ConnectionProviderFactory connectionProviderFactory;
    @Autowired
    ConnectionService connectionService;

    @GET
    @Path("/types")
    @ApiOperation(value = "Returns all connection types available.", notes = "An array of string value is returned representing all possible connection types.  "
            + "A connector type is a top level identifier for distinguishing what the type of a connection "
            + "(e.g. cloud, projecthosting). Every connection has a top level type.")
    public Response getProviderTypes() {
        return Response.ok(ConnectionUtils.PROVIDER_MAP.keySet()).build();
    }

    @GET
    @Path("/providers")
    @ApiOperation(value = "Returns all connection providers available.", notes = "Returns an array of strings listing of connection providers.  Each individual connection  "
            + "describes an identifier for a provider, canonical names for each provider, the type (e.g. cloud"
            + ", projecthosting) that the provider belongs to, and optionally all possible methods of "
            + "authentication for a given provider. Every connection has a provider.", responseClass = "com.streamreduce.rest.dto.response.ConnectionProvidersResponseDTO")
    public Response getProviderList(
            @QueryParam("showAuthTypes") @ApiParam(name = "showAuthTypes", value = "false", defaultValue = "false") boolean showAuthTypes) {

        ConnectionProvidersResponseDTO responseDTO = new ConnectionProvidersResponseDTO();
        List<ConnectionProviderResponseDTO> providers = new ArrayList<>();

        for (ConnectionProvider provider : ConnectionUtils.getAllProviders()) {
            providers.add(ConnectionProviderResponseDTO.toDTO(provider, showAuthTypes));
        }

        responseDTO.setProviders(providers);

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

    /**
     * Supported connection provider types that can be specified are: cloud, projecthosting, feed, gateway
     *
     * @param providerType  the provider type to filter all connection providers by
     * @param showAuthTypes boolean value specifying whether authType details should be included or not
     * @return the list of connection providers
     * @resource.representation.200.doc returned when all connection provider types are successfully returned
     * @response.representation.400.doc Returned when an invalid or non-existent providerType is supplied.
     */
    @GET
    @Path("providers/{providerType}")
    @ApiOperation(value = "Returns a list of connection providers for only the specified type.", notes = "Using any possible value from /connections/types, an array of providers for that type will be "
            + "returned", responseClass = "com.streamreduce.rest.dto.response.ConnectionProvidersResponseDTO")
    public Response getProviderList(
            @ApiParam(name = "type", value = "cloud", required = true) @PathParam("type") String providerType,

            @ApiParam(name = "showAuthTypes", value = "false", defaultValue = "false") @QueryParam("showAuthTypes") boolean showAuthTypes) {

        // We want our caller to know they requested an invalid connection type
        if (!ConnectionUtils.PROVIDER_MAP.containsKey(providerType)) {
            return error("'" + providerType + "' is an invalid connection provider type.",
                    Response.status(Response.Status.BAD_REQUEST));
        }

        ConnectionProvidersResponseDTO responseDTO = new ConnectionProvidersResponseDTO();
        List<ConnectionProviderResponseDTO> providers = new ArrayList<>();

        for (ConnectionProvider provider : ConnectionUtils.getSupportedProviders(providerType)) {
            providers.add(ConnectionProviderResponseDTO.toDTO(provider, showAuthTypes));
        }

        responseDTO.setProviders(providers);

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

    @GET
    @ApiOperation(value = "Returns all connections that the current user hass access to.", responseClass = "com.streamreduce.rest.dto.response.ConnectionResponseDTO")
    public Response listAllConnections() {
        List<ConnectionResponseDTO> connectionsDTO = new ArrayList<>();
        User user = securityService.getCurrentUser();

        List<Connection> connections = connectionService.getConnections(null, user);

        for (Connection connection : connections) {
            // do not include blacklisted connections, these have been "deleted".
            if (!ConnectionUtils.isBlacklisted(user.getAccount(), connection.getId())) {
                connectionsDTO.add(toFullDTO(connection));
            }
        }

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

    /**
     * Returns a list of connection providers of a particular type.
     * Supported connection provider types: cloud, projecthosting, feed, and gateway.
     *
     * @param providerType the connection provider type
     * @return the collection of connections for the given type or an error if something goes wrong
     * @response.representation.200.doc Returned when a list of connections for a type is successfully rendered
     */
    @GET
    @Path("/types/{providerType}")
    @ApiOperation(value = "Returns a list of connections for a given type that the current user has access to", responseClass = "com.streamreduce.rest.dto.response.ConnectionResponseDTO")
    public Response listConnectionsOfType(
            @ApiParam(name = "type", value = "cloud", required = true) @PathParam("type") String providerType) {

        List<ConnectionResponseDTO> connectionsDTO = new ArrayList<>();
        User user = securityService.getCurrentUser();
        List<Connection> connections = connectionService.getConnections(providerType, user);

        for (Connection connection : connections) {
            connectionsDTO.add(toFullDTO(connection));
        }

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

    @GET
    @Path("/externalId/{externalId}")
    @ApiOperation(value = "Returns a list of connections with an externalId matching the externalId in the path that "
            + "the user has access to.", responseClass = "com.streamreduce.rest.dto.response.ConnectionResponseDTO")
    public Response getConnectionsByExternalId(
            @PathParam("externalId") @ApiParam(name = "externalId", required = true) String externalId) {

        List<ConnectionResponseDTO> connectionsDTO = new ArrayList<>();

        User user = securityService.getCurrentUser();
        List<Connection> connections = connectionService.getConnectionsByExternalId(externalId, user);
        for (Connection connection : connections) {
            connectionsDTO.add(toFullDTO(connection));
        }

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

    /**
     * @response.representation.200.doc Returned when a connection for the given id is successfully retrieved
     * @response.representation.400.doc Returned when the id is null when passed in
     * @response.representation.404.doc Returned when you request a connection whose id does not exist for the given user
     */
    @GET
    @Path("{id}")
    @ApiOperation(value = "Returns a connection with the given id.", responseClass = "com.streamreduce.rest.dto.response.ConnectionResponseDTO")
    public Response getConnectionWithId(@PathParam("id") @ApiParam(name = "id", required = true) String id) {
        if (StringUtils.isBlank(id)) {
            return error(ErrorMessages.MISSING_REQUIRED_FIELD, Response.status(Response.Status.BAD_REQUEST));
        }
        ObjectId objectId = new ObjectId(id);

        try {
            return Response.ok(toFullDTO(connectionService.getConnection(objectId))).build();
        } catch (ConnectionNotFoundException e) {
            return error(e.getMessage(), Response.status(Response.Status.NOT_FOUND));
        }
    }

    /**
     * @param json the json payload describing the connection
     * @response.representation.200.doc Returned when a connection is successfully created
     * @response.representation.400.doc Returned when a duplicate connection (or alias) exists, if credentials are invalid, or if the json payload is missing necessary attributes.
     * @response.representation.500.doc Returned a host can't be found from a client supplied url or another unexpected network layer problem occurred connection to host.
     */
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @ApiOperation(value = "Creates a new connection.", notes = "The passed in connection will be checked to make sure it can connect to the specified "
            + "provider with the optionally supplied url credentials.  A new connection must also have an account"
            + " unique alias.", responseClass = "com.streamreduce.rest.dto.response.ConnectionResponseDTO")
    public Response createConnection(@ApiParam(name = "connection", required = true, value = "A JSON object with "
            + "all fields for a connection included with proper values") JSONObject json) {

        User currentUser = securityService.getCurrentUser();
        try {
            Connection connection = new Connection.Builder().mergeWithJSON(json).user(currentUser) //sets user and account
                    .provider(connectionProviderFactory.connectionProviderFromId(getJSON(json, "providerId"))) //sets providerId and type
                    .outboundConfigurations(extractOutboundConfigurationsFromJSON(json)) //set outboundConfigurations if available in the jsonObject
                    .build();

            connectionService.createConnection(connection);
            return Response.ok(toFullDTO(connection)).build();

        } catch (ConnectionExistsException e) {
            return error(e.getMessage(), Response.status(Response.Status.BAD_REQUEST));
        } catch (ConnectionNotFoundException e) {
            return error("A Connection specified in an outboundConfiguration does not exist: " + e.getMessage(),
                    Response.status(Response.Status.BAD_REQUEST));
        } catch (InvalidCredentialsException e) {
            return error(e.getMessage(), Response.status(Response.Status.BAD_REQUEST));
        } catch (UnknownHostException e) {
            return error(e.getMessage() + " is an unknown host. ",
                    Response.status(Response.Status.INTERNAL_SERVER_ERROR));
        } catch (IOException e) {
            return error(e.getMessage(), Response.status(Response.Status.INTERNAL_SERVER_ERROR));
        } catch (IllegalArgumentException e) {
            // This happens with null URLs in project hosting validation (pre-DAO persist)
            ConstraintViolationExceptionResponseDTO dto = new ConstraintViolationExceptionResponseDTO();
            dto.setViolations(ImmutableMap.of("url", e.getMessage()));
            return Response.status(Response.Status.BAD_REQUEST).entity(dto).build();
        } catch (RuntimeException e) {
            //Catch all for runtime exceptions
            e.printStackTrace();
            return error(e.getMessage(), Response.status(Response.Status.BAD_REQUEST));
        }
    }

    /**
     * Creates a new external resource on a given connection if the connection provider supports two-way integration.
     * <p/>
     * Presently only creation of new issues on connections with a provider type of "jira" is supported.
     *
     * @param id the id of the connection to create the external resource
     * @param json the json payload describing the resource to be created
     * @return the newly assigned resource id.
     * @response.representation.200.doc Returned when an external resource on the connection is successfully created
     * @resource.representation.400.doc Returned when the provider does not support creation of the resource
     * @resource.representation.404.doc Returned when the id is not found
     * @resource.representation.500.doc Returned when the resource creation request to external provider failed
     */
    @POST
    @Path("/{id}")
    @Consumes(MediaType.APPLICATION_JSON)
    public Response createExternalResource(@PathParam("id") String id, JSONObject json) {
        if (StringUtils.isBlank(id)) {
            return error(ErrorMessages.MISSING_REQUIRED_FIELD, Response.status(Response.Status.BAD_REQUEST));
        }
        ObjectId objectId = new ObjectId(id);

        AbstractProjectHostingClient projectHostingClient = null;

        try {
            Connection connection = connectionService.getConnection(objectId);

            if (!isOwnerOrAdmin(connection.getUser(), connection.getAccount())) {
                return error(ErrorMessages.APPLICATION_ACCESS_DENIED, Response.status(Response.Status.BAD_REQUEST));
            }

            if (connection.getProviderId().equals(ProviderIdConstants.JIRA_PROVIDER_ID)) {
                projectHostingClient = new JiraClient(connection);

                ProjectHostingIssue issue = new ProjectHostingIssue();

                issue.setType(getJSON(json, "type"));
                issue.setProject("project");
                issue.setSummary("summary");
                issue.setDescription("description");

                try {
                    return Response.ok(((JiraClient) projectHostingClient).createIssue(issue)).build();
                } catch (SOAPException e) {
                    return error("Error creating Jira issue using SOAP API for connection [" + connection.getId()
                            + "]: " + e.getMessage(), Response.status(Response.Status.INTERNAL_SERVER_ERROR));
                }
            } else {
                return error("The connection type for the id specified does not support creating external issues.",
                        Response.status(Response.Status.BAD_REQUEST));
            }

        } catch (ConnectionNotFoundException e) {
            return error(e.getMessage(), Response.status(Response.Status.NOT_FOUND));
        } finally {
            if (projectHostingClient != null) {
                projectHostingClient.cleanUp();
            }
        }
    }

    /**
     * @response.representation.400.doc If a client attempts to update a connection not accessible to the client, or if new credentials for the connection do not work.
     * @response.representation.404.doc Returned when a connection id is not found
     * @response.representation.409.doc If a connection being updated will have no attributes that make it a duplicate of another connection
     * the connection fails validation
     * @resource.representation.500.doc Returned when the resource creation request to external provider failed
     */
    @PUT
    @Path("{id}")
    @ApiOperation(value = "Update an existing connection.", notes = "The response is the new representation of the connection.", responseClass = "com.streamreduce.rest.dto.response.ConnectionResponseDTO")
    public Response updateConnection(@ApiParam(name = "id", required = true) @PathParam("id") String id,

            @ApiParam(name = "connection", required = true, value = "A JSON object with "
                    + "all connection fields to be updated") JSONObject json) {

        if (StringUtils.isBlank(id)) {
            return error(ErrorMessages.MISSING_REQUIRED_FIELD, Response.status(Response.Status.BAD_REQUEST));
        }

        ObjectId objectId = new ObjectId(id);

        try {
            Connection connection = connectionService.getConnection(objectId);

            if (!isOwnerOrAdmin(connection.getUser(), connection.getAccount())) {
                return error(ErrorMessages.APPLICATION_ACCESS_DENIED, Response.status(Response.Status.BAD_REQUEST));
            }

            connection.mergeWithJSON(json);
            mergeOutboundConfigurations(connection, json);

            return Response.ok(toFullDTO(connectionService.updateConnection(connection))).build();
        } catch (ConnectionNotFoundException e) {
            return error(e.getMessage(), Response.status(Response.Status.NOT_FOUND));
        } catch (ConnectionExistsException e) {
            return error(e.getMessage(), Response.status(Response.Status.CONFLICT));
        } catch (InvalidCredentialsException e) {
            return error(e.getMessage(), Response.status(Response.Status.BAD_REQUEST));
        } catch (IOException e) {
            return error(e.getMessage(), Response.status(Response.Status.INTERNAL_SERVER_ERROR));
        } catch (IllegalArgumentException e) {
            // This happens with null URLs in project hosting validation (pre-DAO persist)
            ConstraintViolationExceptionResponseDTO dto = new ConstraintViolationExceptionResponseDTO();
            dto.setViolations(ImmutableMap.of("url", e.getMessage()));
            return Response.status(Response.Status.BAD_REQUEST).entity(dto).build();
        } catch (RuntimeException e) {
            //Catch all for runtime exceptions
            return error(e.getMessage(), Response.status(Response.Status.BAD_REQUEST));
        }
    }

    /**
     * @return the response indicating the result of the deletion
     * @response.representation.200.doc Returned when a connection is successfully deleted
     * @response.representation.400.doc Returned when the specified connection does not exist or is inaccessible to the client.
     * @response.representation.404.doc Returned when the id is not found
     */
    @DELETE
    @Path("{id}")
    @ApiOperation(value = "Deletes a connection.")
    public Response deleteConnection(@PathParam("id") @ApiParam(name = "id", required = true) String id) {

        if (StringUtils.isBlank(id)) {
            return error(ErrorMessages.MISSING_REQUIRED_FIELD, Response.status(Response.Status.BAD_REQUEST));
        }

        ObjectId objectId = new ObjectId(id);

        try {
            Connection connection = connectionService.getConnection(objectId);
            Account account = securityService.getCurrentUser().getAccount();

            // SOBA-1885 resource is PUBLIC and you are the admin.
            // AND you are not in the Nodeable account (because we really can delete those)
            if ((connection.getVisibility().equals(SobaObject.Visibility.PUBLIC)
            // && securityService.hasRole(Roles.ADMIN_ROLE)  // SOBA-1937, allow any user to remove public
            ) && !applicationManager.getUserService().getSuperUser().getAccount().getId().equals(account.getId())) {
                // add to blacklist
                account.appendToPublicConnectionBlacklist(connection.getId());
                applicationManager.getUserService().updateAccount(account);

                // remove messages from inbox if we want
                //                if(connection.getHashtags().contains("#sample")) {
                //                    applicationManager.getMessageService().removeSampleMessages(account, connection.getId());
                //                }

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

            // ACCOUNT or PRIVATE scope and you are not the owner, or admin
            if (!isOwnerOrAdmin(connection.getUser(), connection.getAccount())) {
                return error(ErrorMessages.APPLICATION_ACCESS_DENIED, Response.status(Response.Status.BAD_REQUEST));
            }

            // delete the connection, associated inventory will be removed in next refresh job
            connectionService.deleteConnection(connection);

            return Response.ok().build();
        } catch (ConnectionNotFoundException e) {
            return error(e.getMessage(), Response.status(Response.Status.NOT_FOUND));
        }
    }

    /**
     * @response.representation.400.doc Returned when the client does not specify a connection id or specifies a connection id the client does not have access to
     * @response.representation.404.doc Returned when the connection id cannot be found
     */
    @GET
    @Path("{id}/inventory")
    @ApiOperation(value = "Returns the inventory items for a given connection", notes = "Inventory items represent assets created and managed by the external provider.", responseClass = "com.streamreduce.rest.dto.response.ConnectionInventoryResponseDTO")
    public Response getInventory(@ApiParam(name = "id", required = true) @PathParam("id") String id,

            @ApiParam(name = "id", required = false, defaultValue = "false") @DefaultValue("false") @QueryParam("count") boolean count) {
        if (id == null) {
            return error(ErrorMessages.MISSING_REQUIRED_FIELD, Response.status(Response.Status.BAD_REQUEST));
        }

        ObjectId objectId = new ObjectId(id);

        User currentUser = securityService.getCurrentUser();

        Connection connection;

        try {
            connection = connectionService.getConnection(objectId);

            // if it's public it's ok
            if (!isInAccount(connection.getAccount())
                    && !connection.getVisibility().equals(SobaObject.Visibility.PUBLIC)) {
                return error(ErrorMessages.APPLICATION_ACCESS_DENIED, Response.status(Response.Status.BAD_REQUEST));
            }

        } catch (ConnectionNotFoundException e) {
            return error(e.getMessage(), Response.status(Response.Status.NOT_FOUND));
        }

        InventoryService inventoryService = applicationManager.getInventoryService();
        List<InventoryItem> inventoryItems = inventoryService.getInventoryItems(connection, currentUser);

        if (inventoryItems == null || inventoryItems.size() == 0) {
            return Response.noContent().build();
        } else if (count) {
            return Response.ok(inventoryItems.size()).build();
        } else {
            ConnectionInventoryResponseDTO dto = new ConnectionInventoryResponseDTO();
            List<InventoryItemResponseDTO> inventoryItemDTOs = new ArrayList<>();

            for (InventoryItem inventoryItem : inventoryItems) {
                inventoryItemDTOs.add(toFullDTO(inventoryItem));
            }

            dto.setInventoryItems(inventoryItemDTOs);

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

    /**
     * @response.representation.200.doc Returned when a tag is successfully applied to a connection and all of its inventory items
     * @response.representation.400.doc Returned if no hashtag is specified, or if the client does not have access to the given connection
     * @response.representation.404.doc If the connection with the supplied id does not exist
     */
    @POST
    @Path("{id}/hashtag")
    @Consumes(MediaType.APPLICATION_JSON)
    @ApiOperation(value = "Adds the hashtag to a connection and also to all of its inventory items.")
    @Override
    public Response addTag(@ApiParam(name = "id", required = true) @PathParam("id") String id,

            @ApiParam(name = "hashtag", required = true, value = "A JSON object with a hashtag field "
                    + " property that contains the hashtag to be added") JSONObject json) {

        String hashtag = getJSON(json, HASHTAG);

        if (isEmpty(hashtag)) {
            return error("Hashtag payload is empty", Response.status(Response.Status.BAD_REQUEST));
        }

        try {
            Connection connection = connectionService.getConnection(new ObjectId(id));
            User user = securityService.getCurrentUser();

            if (!isInAccount(connection.getAccount())) {
                return error(ErrorMessages.APPLICATION_ACCESS_DENIED, Response.status(Response.Status.BAD_REQUEST));
            }
            connectionService.addHashtag(connection, user, hashtag);

            //  do this in the resources because only users are doing this right now.
            //  probably need to move to the service layer at some point
            List items = applicationManager.getInventoryService().getInventoryItems(connection, user);
            if (items != null) {
                for (Object abstractInventoryItem : items) {
                    applicationManager.getInventoryService().addHashtag((InventoryItem) abstractInventoryItem, user,
                            hashtag);
                }
            }

        } catch (ConnectionNotFoundException e) {
            return error("No connection with the provided id (" + id.toString() + ") could be found.",
                    Response.status(Response.Status.NOT_FOUND));
        }

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

    /**
     * @response.representation.200.doc Returned when a list of hashtags for a given connection is successfully rendered
     * @response.representation.400.doc Returned if a connection id isn't specified or the client does not have access to the connection.
     * @response.representation.404.doc If the connection with the supplied id does not exist
     */
    @GET
    @Path("{id}/hashtag")
    @ApiOperation(value = "Retrieves a list of all hashtags for a given connection.")
    @Override
    public Response getTags(@PathParam("id") @ApiParam(name = "id", required = true) String id) {

        if (StringUtils.isBlank(id)) {
            return error(ErrorMessages.MISSING_REQUIRED_FIELD, Response.status(Response.Status.BAD_REQUEST));
        }

        ObjectId objectId = new ObjectId(id);
        Set<String> tags;
        Connection connection;
        try {
            connection = connectionService.getConnection(objectId);

            if (!isInAccount(connection.getAccount())) {
                return error(ErrorMessages.APPLICATION_ACCESS_DENIED, Response.status(Response.Status.BAD_REQUEST));
            }

        } catch (ConnectionNotFoundException e) {
            return error("No connection with the provided id (" + objectId.toString() + ") could be found.",
                    Response.status(Response.Status.NOT_FOUND));
        }

        tags = connection.getHashtags();

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

    }

    /**
     * @response.representation.200.doc Returned when a hashtag is successfully removed from a connection and all of its inventory
     * @response.representation.400.doc Returned if a connection id isn't specified or the client does not have access to the connection.
     * @response.representation.404.doc If the connection with the supplied id does not exist
     */
    @DELETE
    @Path("{id}/hashtag/{tagname}")
    @ApiOperation(value = "Deletes a hashtag from a connection and all of its inventory items.")
    @Override
    public Response removeTag(@ApiParam(name = "id", required = true) @PathParam("id") String id,

            @ApiParam(name = "tagname", required = true) @PathParam("tagname") String hashtag) {

        if (id == null) {
            return error(ErrorMessages.MISSING_REQUIRED_FIELD, Response.status(Response.Status.BAD_REQUEST));
        }

        if (isEmpty(hashtag)) {
            return error("Hashtag payload is empty", Response.status(Response.Status.BAD_REQUEST));
        }

        try {
            ObjectId objectId = new ObjectId(id);
            Connection connection = connectionService.getConnection(objectId);

            if (!isInAccount(connection.getAccount())) {
                return error(ErrorMessages.APPLICATION_ACCESS_DENIED, Response.status(Response.Status.BAD_REQUEST));
            }

            User user = securityService.getCurrentUser();
            connectionService.removeHashtag(connection, user, hashtag);

            //  do this in the resources because only users are doing this right now.
            //  probably need to move to the service layer at some point
            List items = applicationManager.getInventoryService().getInventoryItems(connection, user);
            if (items != null) {
                for (Object abstractInventoryItem : items) {
                    applicationManager.getInventoryService().removeHashtag((InventoryItem) abstractInventoryItem,
                            user, hashtag);
                }
            }

        } catch (ConnectionNotFoundException e) {
            return error("No connection with the provided id (" + id + ") could be found.",
                    Response.status(Response.Status.NOT_FOUND));
        }

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

    /**
     * @response.representation.200.doc Returned if the request to refresh inventory immediately was granted.
     * @response.representation.400.doc Returned if a connection id isn't specified or the client does not have access to the connection.
     * @response.representation.404.doc If the connection with the supplied id does not exist
     * @response.representation.500.doc If the refresh of the connection's inventory could not be scheduled
     */
    @POST
    @Path("{id}/inventory/refresh")
    @ApiOperation(value = "Immediately refreshes the inventory for the given connection")
    public Response refreshInventory(@PathParam("id") @ApiParam(name = "id", required = true) String id) {

        if (id == null) {
            return error(ErrorMessages.MISSING_REQUIRED_FIELD, Response.status(Response.Status.BAD_REQUEST));
        }

        ObjectId objectId = new ObjectId(id);

        try {
            Connection connection;
            try {
                connection = connectionService.getConnection(objectId);

            } catch (ConnectionNotFoundException e) {
                return error("No connection with the provided id (" + objectId.toString() + ") could be found.",
                        Response.status(Response.Status.NOT_FOUND));
            }

            // only the owner can do this...
            if (!isOwner(connection.getUser())) {
                return error(ErrorMessages.APPLICATION_ACCESS_DENIED, Response.status(Response.Status.BAD_REQUEST));
            }

            if (!connection.getType().equals(ConnectionTypeConstants.FEED_TYPE)
                    || !connection.getType().equals(ConnectionTypeConstants.GATEWAY_TYPE)) {
                connectionService.fireOneTimeHighPriorityJobForConnection(connection);
            }
        } catch (ConnectionNotFoundException e) {
            return error(e.getMessage(), Response.status(Response.Status.NOT_FOUND));
        } catch (InvalidCredentialsException e) {
            return error(e.getMessage(), Response.status(Response.Status.BAD_REQUEST));
        } catch (IOException e) {
            return error(e.getMessage(), Response.status(Response.Status.INTERNAL_SERVER_ERROR));
        }

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

    /**
     * Expects a jsonObject that contains a child element of outboundConfigurations and converts it to an
     * OutboundConfiguration[] to be used as varargs to
     * {@link com.streamreduce.core.model.Connection.Builder#outboundConfigurations(com.streamreduce.core.model.OutboundConfiguration...)}
     *
     * @param jsonObject a JSONObject representing an OutboundConfiguration received from the REST API.
     * @return OutboundConfiguration[] containing all transformed jsonobjects into OutboundConfigurations
     */
    private OutboundConfiguration[] extractOutboundConfigurationsFromJSON(JSONObject jsonObject)
            throws ConnectionNotFoundException {
        //There's really no better place to put this unless the model objects start including services so they can
        //lazily load fields.

        if (jsonObject == null || !jsonObject.containsKey("outboundConfigurations")) {
            return new OutboundConfiguration[0];
        }

        List<OutboundConfiguration> outboundConfigurationList = new ArrayList<>();
        for (Object o : jsonObject.getJSONArray("outboundConfigurations")) {
            OutboundConfiguration outboundConfiguration = extractOutboundConfigurationFromJSON((JSONObject) o);
            outboundConfigurationList.add(outboundConfiguration);
        }
        return outboundConfigurationList.toArray(new OutboundConfiguration[outboundConfigurationList.size()]);
    }

    private OutboundConfiguration extractOutboundConfigurationFromJSON(
            JSONObject outboundConfigurationAsJSONObject) {
        OutboundConfiguration.Builder outboundConfigurationBuilder = new OutboundConfiguration.Builder()
                .protocol(outboundConfigurationAsJSONObject.getString("protocol"));

        List<OutboundDataType> outboundDataTypes = new ArrayList<>();
        for (Object dataTypeObj : outboundConfigurationAsJSONObject.getJSONArray("dataTypes")) {
            String dataType = ((String) dataTypeObj).toUpperCase();
            outboundDataTypes.add(OutboundDataType.valueOf(dataType));
        }
        outboundConfigurationBuilder
                .dataTypes(outboundDataTypes.toArray(new OutboundDataType[outboundDataTypes.size()]));

        if (outboundConfigurationAsJSONObject.containsKey("credentials")) {
            JSONObject credentialsJSONObject = outboundConfigurationAsJSONObject.getJSONObject("credentials");
            String username = credentialsJSONObject.containsKey("username")
                    ? credentialsJSONObject.getString("username")
                    : null;
            String password = credentialsJSONObject.containsKey("password")
                    ? credentialsJSONObject.getString("password")
                    : null;
            ConnectionCredentials credentials = new ConnectionCredentials(username, password);
            outboundConfigurationBuilder.credentials(credentials);
        }

        //Non-required fields
        if (outboundConfigurationAsJSONObject.containsKey("destination")) {
            outboundConfigurationBuilder.destination(outboundConfigurationAsJSONObject.getString("destination"));
        }
        if (outboundConfigurationAsJSONObject.containsKey("namespace")) {
            outboundConfigurationBuilder.namespace(outboundConfigurationAsJSONObject.getString("namespace"));
        }
        return outboundConfigurationBuilder.build();
    }

    @SuppressWarnings("unchecked")
    private void mergeOutboundConfigurations(Connection connection, JSONObject json)
            throws ConnectionNotFoundException {
        if (!json.containsKey("outboundConfigurations")) {
            return;
        }

        // build a map of outbound configs keyed on the protocol + destination
        Map<String, OutboundConfiguration> currentConfigs = new HashMap<>();
        if (!CollectionUtils.isEmpty(connection.getOutboundConfigurations())) {
            for (OutboundConfiguration outboundConfiguration : connection.getOutboundConfigurations()) {
                String key = outboundConfiguration.getProtocol();
                currentConfigs.put(key, outboundConfiguration);
            }
        }

        JSONArray outboundConfigurations = json.getJSONArray("outboundConfigurations");
        for (Iterator<JSONObject> iter = outboundConfigurations.iterator(); iter.hasNext();) {
            JSONObject outboundConfiguration = iter.next();
            if (outboundConfiguration.containsKey("protocol")) {
                // do we know about this config already?
                String key = outboundConfiguration.getString("protocol");
                OutboundConfiguration configuration = null;
                if (!currentConfigs.containsKey(key)) {
                    configuration = extractOutboundConfigurationFromJSON(outboundConfiguration);
                    connection.addOutboundConfiguration(configuration);
                } else {
                    configuration = currentConfigs.get(key);
                    configuration.mergeWithJSON(outboundConfiguration);
                }
            }
        }
    }
}