SendMessageOperation.java
/*
* Copyright 2015 Aroma Tech.
*
* 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 tech.aroma.application.service.operations;
import com.datastax.driver.core.utils.UUIDs;
import java.time.Instant;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.inject.Inject;
import org.apache.thrift.TException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sir.wellington.alchemy.collections.sets.Sets;
import tech.aroma.data.ApplicationRepository;
import tech.aroma.data.FollowerRepository;
import tech.aroma.data.InboxRepository;
import tech.aroma.data.MessageRepository;
import tech.aroma.data.UserRepository;
import tech.aroma.thrift.Application;
import tech.aroma.thrift.LengthOfTime;
import tech.aroma.thrift.Message;
import tech.aroma.thrift.TimeUnit;
import tech.aroma.thrift.User;
import tech.aroma.thrift.application.service.SendMessageRequest;
import tech.aroma.thrift.application.service.SendMessageResponse;
import tech.aroma.thrift.authentication.ApplicationToken;
import tech.aroma.thrift.authentication.TokenType;
import tech.aroma.thrift.authentication.service.AuthenticationService;
import tech.aroma.thrift.authentication.service.GetTokenInfoRequest;
import tech.aroma.thrift.authentication.service.GetTokenInfoResponse;
import tech.aroma.thrift.events.ApplicationSentMessage;
import tech.aroma.thrift.events.Event;
import tech.aroma.thrift.events.EventType;
import tech.aroma.thrift.exceptions.InvalidArgumentException;
import tech.aroma.thrift.exceptions.InvalidTokenException;
import tech.aroma.thrift.exceptions.OperationFailedException;
import tech.aroma.thrift.functions.TokenFunctions;
import tech.aroma.thrift.message.service.MessageServiceConstants;
import tech.aroma.thrift.notification.service.NotificationService;
import tech.aroma.thrift.notification.service.SendNotificationRequest;
import tech.sirwellington.alchemy.thrift.operations.ThriftOperation;
import static tech.sirwellington.alchemy.arguments.Arguments.checkThat;
import static tech.sirwellington.alchemy.arguments.assertions.Assertions.notNull;
import static tech.sirwellington.alchemy.arguments.assertions.StringAssertions.nonEmptyString;
import static tech.sirwellington.alchemy.arguments.assertions.StringAssertions.stringWithLengthGreaterThanOrEqualTo;
/**
*
* @author SirWellington
*/
final class SendMessageOperation implements ThriftOperation<SendMessageRequest, SendMessageResponse>
{
private final static Logger LOG = LoggerFactory.getLogger(SendMessageOperation.class);
private final AuthenticationService.Iface authenticationService;
private final ApplicationRepository appRepo;
private final InboxRepository inboxRepo;
private final MessageRepository messageRepo;
private final FollowerRepository followerRepo;
private final UserRepository userRepo;
private final NotificationService.Iface notificationService;
@Inject
SendMessageOperation(AuthenticationService.Iface authenticationService,
ApplicationRepository appRepo,
FollowerRepository followerRepo,
InboxRepository inboxRepo,
MessageRepository messageRepo,
UserRepository userRepo,
NotificationService.Iface notificationService)
{
checkThat(authenticationService, appRepo, followerRepo, inboxRepo, messageRepo, userRepo, notificationService)
.are(notNull());
this.authenticationService = authenticationService;
this.appRepo = appRepo;
this.messageRepo = messageRepo;
this.followerRepo = followerRepo;
this.inboxRepo = inboxRepo;
this.userRepo = userRepo;
this.notificationService = notificationService;
}
/*
* TODO: Add Rate Limiting
*/
@Override
public SendMessageResponse process(SendMessageRequest request) throws TException
{
checkThat(request)
.throwing(InvalidArgumentException.class)
.usingMessage("request missing")
.is(notNull());
checkThat(request.applicationToken)
.throwing(InvalidTokenException.class)
.usingMessage("missing Application Token")
.is(notNull());
GetTokenInfoResponse tokenInfo = tryToGetTokenInfo(request.applicationToken);
ApplicationToken appToken = TokenFunctions.authTokenToAppTokenFunction().apply(tokenInfo.token);
String applicationId = appToken.applicationId;
checkAppId(applicationId);
Message message = createMessageFrom(request, appToken);
messageRepo.saveMessage(message, MessageServiceConstants.DEFAULT_MESSAGE_LIFETIME);
LOG.debug("Message successfully stored in repository");
storeInFollowerInboxes(message);
SendNotificationRequest sendNotificationRequest = createNotificationRequestFor(message);
tryToSendNotification(sendNotificationRequest);
SendMessageResponse response = new SendMessageResponse()
.setMessageId(message.messageId);
return response;
}
private GetTokenInfoResponse tryToGetTokenInfo(ApplicationToken applicationToken) throws InvalidTokenException,
OperationFailedException
{
GetTokenInfoRequest getTokenInfoRequest = new GetTokenInfoRequest()
.setTokenId(applicationToken.tokenId)
.setTokenType(TokenType.APPLICATION);
GetTokenInfoResponse tokenInfo;
try
{
tokenInfo = authenticationService.getTokenInfo(getTokenInfoRequest);
}
catch (InvalidTokenException ex)
{
LOG.warn("Application Token is Invalid: [{}]", applicationToken, ex);
throw ex;
}
catch (TException ex)
{
LOG.error("Failed to get info for Token [{}]", applicationToken, ex);
throw new OperationFailedException("Could not get token info: " + ex.getMessage());
}
checkThat(tokenInfo, tokenInfo.token)
.throwing(OperationFailedException.class)
.usingMessage("AuthenticationService Response is missing Token Info")
.are(notNull());
checkThat(tokenInfo.token.ownerId)
.throwing(OperationFailedException.class)
.usingMessage("missing Token Info")
.is(nonEmptyString());
return tokenInfo;
}
private Message createMessageFrom(SendMessageRequest request, ApplicationToken token)
{
UUID messageId = UUIDs.timeBased();
Message message = new Message()
.setApplicationId(token.applicationId)
.setApplicationName(token.applicationName)
.setMessageId(messageId.toString())
.setBody(request.body)
.setTitle(request.title)
.setUrgency(request.urgency)
.setTimeOfCreation(request.timeOfMessage)
.setTimeMessageReceived(Instant.now().toEpochMilli())
.setHostname(request.hostname)
.setMacAddress(request.macAddress);
return message;
}
private SendNotificationRequest createNotificationRequestFor(Message message)
{
ApplicationSentMessage applicationSentMessage = new ApplicationSentMessage()
.setApplicationId(message.applicationId)
.setApplicationName(message.applicationName)
.setMessage(message.body);
EventType eventType = new EventType();
eventType.setApplicationSentMessage(applicationSentMessage);
Event event = new Event()
.setTimestamp(Instant.now().getEpochSecond())
.setEventId("")
.setEventType(eventType);
return new SendNotificationRequest().setEvent(event);
}
private void tryToSendNotification(SendNotificationRequest sendNotificationRequest)
{
try
{
notificationService.sendNotification(sendNotificationRequest);
}
catch (TException ex)
{
LOG.warn("Failed to send Notification request: {}", sendNotificationRequest, ex);
}
}
private void checkAppId(String applicationId) throws OperationFailedException
{
checkThat(applicationId)
.throwing(OperationFailedException.class)
.usingMessage("Could not get Application ID from Token")
.is(nonEmptyString())
.is(stringWithLengthGreaterThanOrEqualTo(10));
}
private void storeInFollowerInboxes(Message message) throws TException
{
String appId = message.applicationId;
Set<User> followers = Sets.toSet(followerRepo.getApplicationFollowers(appId));
Set<User> owners = getOwnerForApp(appId);
followers.addAll(owners);
followers.parallelStream()
.forEach(user -> this.tryToSaveInInbox(message, user));
LOG.debug("Store Message in the Inboxes of {} users", followers.size());
}
private Set<User> getOwnerForApp(String appId) throws TException
{
Application app = appRepo.getById(appId);
return app.owners
.stream()
.map(this::toUser)
.collect(Collectors.toSet());
}
private void tryToSaveInInbox(Message message, User user)
{
LengthOfTime lifetime = new LengthOfTime(TimeUnit.HOURS, 12);
try
{
inboxRepo.saveMessageForUser(user, message, lifetime);
}
catch (TException ex)
{
LOG.error("Failed to save message {} in Inbox of User {}", message, user, ex);
}
}
private User toUser(String userId)
{
try
{
return userRepo.getUser(userId);
}
catch (TException ex)
{
LOG.warn("Could not find user With ID [{}]", userId, ex);
return new User().setUserId(userId);
}
}
}