/*
* 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.core.service;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.streamreduce.ConnectionNotFoundException;
import com.streamreduce.ProviderIdConstants;
import com.streamreduce.connections.AuthType;
import com.streamreduce.connections.ConnectionProviderFactory;
import com.streamreduce.connections.ExternalIntegrationConnectionProvider;
import com.streamreduce.connections.OAuthEnabledConnectionProvider;
import com.streamreduce.core.dao.ConnectionDAO;
import com.streamreduce.core.event.EventId;
import com.streamreduce.core.model.Account;
import com.streamreduce.core.model.Connection;
import com.streamreduce.core.model.ConnectionCredentials;
import com.streamreduce.core.model.ConnectionCredentialsEncrypter;
import com.streamreduce.core.model.Event;
import com.streamreduce.core.model.InventoryItem;
import com.streamreduce.core.model.OutboundConfiguration;
import com.streamreduce.core.model.SobaObject;
import com.streamreduce.core.model.User;
import com.streamreduce.core.model.messages.MessageType;
import com.streamreduce.core.service.exception.ConnectionExistsException;
import com.streamreduce.core.service.exception.InvalidCredentialsException;
import com.streamreduce.util.AWSClient;
import com.streamreduce.util.ExternalIntegrationClient;
import com.streamreduce.util.HashtagUtil;
import com.streamreduce.util.InvalidOutboundConfigurationException;
import com.streamreduce.util.SecurityUtil;
import com.streamreduce.util.WebHDFSClient;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import javax.annotation.Nullable;
import org.apache.commons.collections.CollectionUtils;
import org.bson.types.ObjectId;
import org.scribe.model.Token;
import org.scribe.model.Verifier;
import org.scribe.oauth.OAuthService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/**
* Implementation of {@link ConnectionService}.
*/
@Service("connectionService")
public class ConnectionServiceImpl implements ConnectionService {
protected transient Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ConnectionDAO connectionDAO;
@Autowired
private ConnectionProviderFactory connectionProviderFactory;
@Autowired
private InventoryService inventoryService;
@Autowired
private MessageService messageService;
@Autowired
private EventService eventService;
@Autowired
private OAuthTokenCacheService cacheService;
/**
* {@inheritDoc}
*/
@Override
public Connection createConnection(Connection connection)
throws ConnectionExistsException, InvalidCredentialsException, IOException {
addDefaultProtocolToURLIfMissing(connection);
checkForDuplicate(connection);
setCredentialsIfOauth(connection);
setCredentialsIfGateway(connection);
validateExternalIntegrationConnections(connection);
validateOutboundConfigurations(connection.getOutboundConfigurations());
// Add provider id to hashtags just in case it wasn't done already
String providerId = connection.getProviderId();
connection.addHashtag(providerId);
connectionDAO.save(connection);
decryptCredentials(connection);
// Create the event stream entry
Event event = eventService.createEvent(EventId.CREATE, connection, null);
// Create message
messageService.sendConnectionMessage(event, connection);
return connection;
}
private void decryptCredentials(Connection connection) {
ConnectionCredentialsEncrypter credentialsEncrypter = new ConnectionCredentialsEncrypter();
// force decryption until we upgrade to morphia 1.0
if (connection.getCredentials() != null) {
credentialsEncrypter.decrypt(connection.getCredentials());
}
if (CollectionUtils.isNotEmpty(connection.getOutboundConfigurations())) {
for (OutboundConfiguration outboundConfiguration : connection.getOutboundConfigurations()) {
if (outboundConfiguration.getCredentials() != null) {
credentialsEncrypter.decrypt(outboundConfiguration.getCredentials());
}
}
}
}
private void addDefaultProtocolToURLIfMissing(Connection connection) {
// For Feed connections, hold the user's hand by putting the default protocol (http://) on the URL if not there
if (connection.getProviderId().equals(ProviderIdConstants.FEED_PROVIDER_ID)) {
// Add the http:// protocol if missing
try {
new URL(connection.getUrl());
} catch (MalformedURLException e) {
if (connection.getUrl() != null && !connection.getUrl().contains("://")) {
connection.setUrl("http://" + connection.getUrl().trim());
}
}
}
}
/**
* Initializes values in connection.credentials for Oauth connections. There are three possible things that
* can happen (not taking exceptions into account).
* <p/>
* <ol>
* <li>If connection.authType isn't OAUTH, this method returns immediately without mutating anything in
* connection</li>
* <li>If connection.credentials.verifier is null/blank, we assume that the credentials.oauthToken
* and credentials.oauthTokenSecret fields were previously valid and are meant to be re-used.</li>
* <li>If connection.credentials.verifier had content, we assume that we are in the last steps of
* an OAuth handshake with the connection provider and real credentials will retrieved using that
* verification field.</li>
* </ol>
*
* @param connection A connection currently being created.
*/
private void setCredentialsIfOauth(Connection connection) {
if (connection.getAuthType() != AuthType.OAUTH) {
return;
}
if (StringUtils.hasText(connection.getCredentials().getOauthVerifier())) {
finishHandshakeAndSetRealOauthTokens(connection);
}
setIdentityForOauthConnection(connection);
}
private void finishHandshakeAndSetRealOauthTokens(Connection connection) {
ConnectionCredentials credentials = connection.getCredentials();
OAuthEnabledConnectionProvider oauthProvider =
connectionProviderFactory.oauthEnabledConnectionProviderFromId(connection.getProviderId());
OAuthService oAuthService = oauthProvider.getOAuthService();
Token requestToken = cacheService.retrieveAndRemoveToken(credentials.getOauthToken());
Token token = oAuthService.getAccessToken(requestToken, new Verifier(credentials.getOauthVerifier()));
oauthProvider.updateCredentials(credentials, token);
}
private void setIdentityForOauthConnection(Connection connection) {
OAuthEnabledConnectionProvider oauthProvider =
connectionProviderFactory.oauthEnabledConnectionProviderFromId(connection.getProviderId());
connection.getCredentials().setIdentity(oauthProvider.getIdentityFromProvider(connection));
}
private void setCredentialsIfGateway(Connection connection) {
if (connection.getAuthType() != null && connection.getAuthType().equals(AuthType.API_KEY)) {
// auto generate an API key for them
if (connection.getCredentials() == null) {
connection.setCredentials(new ConnectionCredentials());
}
// we also use a user agent as a validation factor
// so when we later validate the token, we also validate the user agent
String apiToken = SecurityUtil.issueRandomAPIToken();
connection.getCredentials().setIdentity(apiToken);
}
}
/**
* {@inheritDoc}
*/
@Override
public List<Connection> getConnections(@Nullable String type) {
// TODO: Event handling
return connectionDAO.allConnectionsOfType(type);
}
@Override
public List<Connection> getPublicConnections(@Nullable String type) {
return connectionDAO.allPublicConnectionsOfType(type);
}
/**
* {@inheritDoc}
*/
@Override
public List<Connection> getConnections(@Nullable String type, User user) {
// TODO: Event handling
return connectionDAO.forTypeAndUser(type, user);
}
@Override
public List<Connection> getAccountConnections(Account account) {
return connectionDAO.forAccount(account);
}
/**
* {@inheritDoc}
*/
@Override
public Connection getConnection(ObjectId id) throws ConnectionNotFoundException {
Connection connection = connectionDAO.get(id);
if (connection == null) {
// TODO: Event handling
throw new ConnectionNotFoundException(id == null ? "null" : id.toString());
}
return connection;
}
/**
* {@inheritDoc}
*/
@Override
public Connection updateConnection(Connection connection) throws ConnectionExistsException,
InvalidCredentialsException, IOException {
return updateConnection(connection, false);
}
/**
* {@inheritDoc}
*/
@Override
public Connection updateConnection(Connection connection, boolean silentUpdate) throws ConnectionExistsException,
InvalidCredentialsException, IOException {
connection.setSilentUpdate(silentUpdate);
if (!silentUpdate) {
checkForDuplicate(connection);
validateExternalIntegrationConnections(connection);
validateOutboundConfigurations(connection.getOutboundConfigurations());
}
connectionDAO.save(connection);
decryptCredentials(connection);
if (!silentUpdate) {
// Create the event stream entry
Event event = eventService.createEvent(EventId.UPDATE, connection, null);
// Create message
messageService.sendConnectionMessage(event, connection);
}
return connection;
}
private void validateExternalIntegrationConnections(Connection connection) throws InvalidCredentialsException, IOException {
ExternalIntegrationConnectionProvider connectionProvider;
try {
connectionProvider = connectionProviderFactory.externalIntegrationConnectionProviderFromId(connection.getProviderId());
} catch (Exception e) {
logger.info("Connection with providerId of " + connection.getProviderId() + " does not " +
"support validation via an external client.");
return;
}
ExternalIntegrationClient externalClient = connectionProvider.getClient(connection);
externalClient.validateConnection();
externalClient.cleanUp();
}
private void validateOutboundConfigurations(Set<OutboundConfiguration> outboundConfigurations) throws InvalidCredentialsException, IOException {
if (CollectionUtils.isEmpty(outboundConfigurations)) {
return;
}
ExternalIntegrationClient externalClient = null;
try {
for (OutboundConfiguration outboundConfiguration : outboundConfigurations) {
if (outboundConfiguration.getProtocol().equals("s3")) {
AWSClient awsClient = new AWSClient(outboundConfiguration);
externalClient = awsClient;
externalClient.validateConnection();
try {
awsClient.createBucket(outboundConfiguration);
} catch (IllegalStateException e) { //thrown when a bucket name is already taken
throw new InvalidOutboundConfigurationException(e.getMessage(), e);
}
} else if (outboundConfiguration.getProtocol().equals("webhdfs")) {
externalClient = new WebHDFSClient(outboundConfiguration);
externalClient.validateConnection();
}
}
} finally {
if (externalClient != null) {
externalClient.cleanUp();
}
}
}
public void deleteConnection(Connection connection) {
deleteConnectionInventory(connection.getId());
Event event = eventService.createEvent(EventId.DELETE, connection, null);
messageService.sendConnectionMessage(event, connection);
connectionDAO.delete(connection);
}
/**
* {@inheritDoc}
*/
@Override
public void deleteConnectionInventory(ObjectId connectionId) {
List<InventoryItem> inventoryItems = inventoryService.getInventoryItems(connectionId);
for (InventoryItem inventoryItem : inventoryItems) {
inventoryService.deleteInventoryItem(inventoryItem);
}
}
/**
* Checks a given connection against all existing connections in a given account to ensure a duplicate connection
* does not already exist. A connection is considered a duplicate of another connection when
* <ul>
* <li>Aliases are the same</li>
* <li>If there are credentials with the connection, url+credentials must not be the same</li>
* <li>If there are no credentials with the connection, url must be not be the same</li>
* </ul>
*
* @param connection a potential new/updated Connection
* @throws ConnectionExistsException when a duplicate connection is detected.
*/
protected void checkForDuplicate(Connection connection) throws ConnectionExistsException {
List<Connection> connections = connectionDAO.forTypeAndUser(connection.getType(), connection.getUser());
for (Connection otherConnection : connections) {
// We use this for create and update so id might not be set yet
try {
// If the connection being compared against is the same as the one being updated, do not compare
if (getConnection(connection.getId()) != null && otherConnection.getId().equals(connection.getId())) {
continue;
}
} catch (ConnectionNotFoundException e) {
// Should not matter in this context
}
checkForEqualAlias(connection, otherConnection);
String cUrl = connection.getUrl() != null ? connection.getUrl().trim().toLowerCase() : "";
String oUrl = otherConnection.getUrl() != null ? otherConnection.getUrl().trim().toLowerCase() : "";
// Duplicate if credentials aren't set and URL exists elsewhere in account.
if (credsAreNullOrBlank(connection) && credsAreNullOrBlank(otherConnection) && cUrl.equals(oUrl)) {
throw ConnectionExistsException.Factory.duplicateCredentials(connection);
}
// Duplicate if both credentials and URLs are equal.
// If the URLs are the same then make sure credentials are not
if (cUrl.equals(oUrl)) {
ConnectionCredentials cCreds = connection.getCredentials();
ConnectionCredentials oCreds = otherConnection.getCredentials();
if ((cCreds == null && oCreds == null) || (cCreds != null && oCreds != null && cCreds.equals(oCreds))) {
throw ConnectionExistsException.Factory.duplicateCredentials(connection);
}
}
}
}
private boolean credsAreNullOrBlank(Connection connection) {
return connection.getCredentials() == null || connection.getCredentials().getIdentity() == null ||
connection.getCredentials().getIdentity().trim().equals("");
}
private void checkForEqualAlias(Connection newConnection, Connection existingConnection)
throws ConnectionExistsException {
if (newConnection.getAlias() != null && existingConnection.getAlias() != null &&
newConnection.getAlias().equalsIgnoreCase(existingConnection.getAlias())) {
throw ConnectionExistsException.Factory.duplicateAlias(newConnection);
}
}
@Override
public void fireOneTimeHighPriorityJobForConnection(Connection connection) throws ConnectionNotFoundException,
InvalidCredentialsException, IOException {
inventoryService.refreshInventoryItemCache(connection);
inventoryService.pullInventoryItemActivity(connection);
}
@Override
public void addHashtag(Connection target, SobaObject tagger, String tag) {
target.addHashtag(tag);
handleHashtagEvent(EventId.HASHTAG_ADD, target, tagger, tag);
try {
updateConnection(target, true);
} catch (ConnectionExistsException | IOException | InvalidCredentialsException e) {
logger.error(e.getMessage());
}
}
@Override
public void removeHashtag(Connection target, SobaObject tagger, String tag) {
String normalizedTag = HashtagUtil.normalizeTag(tag);
target.removeHashtag(normalizedTag);
handleHashtagEvent(EventId.HASHTAG_DELETE, target, tagger, normalizedTag);
try {
updateConnection(target, true);
} catch (ConnectionExistsException | IOException | InvalidCredentialsException e) {
logger.error(e.getMessage());
}
}
private void handleHashtagEvent(EventId eventId, Connection target, SobaObject tagger, String tag) {
// Create the event
Map<String, Object> eventContext = new HashMap<>();
if (eventId == EventId.HASHTAG_ADD) {
eventContext.put("addedHashtag", tag);
} else if (eventId == EventId.HASHTAG_DELETE) {
eventContext.put("deletedHashtag", tag);
}
Event event = eventService.createEvent(eventId, target, eventContext);
// Create the message
// TODO: Should this use MessageService#sendConnectionMessage(Event, Connection)?
messageService.sendAccountMessage(event, tagger, target, new Date().getTime(),
MessageType.CONNECTION, target.getHashtags(), null);
}
@Override
public List<Connection> getConnectionsByExternalId(String externalId, final User user) {
List<Connection> connections = connectionDAO.getByExternalId(externalId);
return Lists.newArrayList(Iterables.filter(connections, new Predicate<Connection>() {
@Override
public boolean apply(@Nullable Connection connection) {
return (connection != null && connection.getUser().equals(user));
}
}));
}
}